programName = 'ezColS251110aP.py'
programRevision = programName

# ezRA - Easy Radio Astronomy ezCol Data COLlector program,
#   COLlect radio signals into integrated frequency spectrum data ezRA .txt files.
# https://github.com/tedcline/ezRA

# Copyright (c) 2025, Ted Cline   TedClineGit@gmail.com

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

# Modified from Victor Boesen's https://github.com/byggemandboesen/H-line-software

# Thanks to Todd Ullery, ezColX.py was an experimental multiple process version of ezCol.py ,
# to improve graphic dashboard responsiveness, 221130 and 221202.

# ezColS251110aP,
# ezColS251107cPeeee, SDRplay LTO15
# ezColS251107aAeeee, Airspy
# ezColS251104aP, sdrplay gain to 5
# ezColS251103aP, sdrplay gain from 30 to 15
# ezColS251031cP, sdrplay works?
#       driver = 'sdrplay'  # this ezColS251031cP, default 31000 in 4.8 seconds with 10MHz to 256
# ezColS251031bP
# ezColS251031aP, SDRplay RSP2/RSP2pro
#   lsusb
#       Bus 003 Device 004: ID 1df7:3010 SDRplay RSP2/RSP2pro
#   https://www.google.com/search?q=SoapySDRPlay3+sudo+make+install&oq=SoapySDRPlay3++sudo+make+install
#   git clone https://github.com/pothosware/SoapySDRPlay3.git
#   cd SoapySDRPlay3
#   mkdir build
#   cd build
#   cmake ..
#   make
#       error: ‘IsValid’ was not declared in this scope
#           /home/a/Downloads/SoapySDRPlay3/Registration.cpp
#           gedit  /home/a/Downloads/SoapySDRPlay3/Registration.cpp  &
#               and changed
#                   // if (not rspDevs[i].valid) continue;
#                   if (not IsValid(rspDevs[i])) continue;
#               to
#                   if (not rspDevs[i].valid) continue;
#                   //if (not IsValid(rspDevs[i])) continue;
#   sudo make install
#       [100%] Built target sdrPlaySupport
#       -- Installing: /usr/local/lib/SoapySDR/modules0.8/libsdrPlaySupport.so
#   SoapySDRUtil --info
#       Available factories... airspy, audio, bladerf, hackrf, lime, miri, osmosdr, redpitaya, remote, rfspace, rtlsdr, sdrplay, uhd
#   SoapySDRUtil --probe=driver=sdrplay
#       Antennas: Antenna A, Antenna B, Hi-Z
#       Full gain range: [0, 47] dB
#         IFGR gain range: [20, 59] dB
#         RFGR gain range: [0, 8] dB
#       Full freq range: [0.001, 2000] MHz
#         RF freq range: [0.001, 2000] MHz
#       Sample rates: 0.0625, 0.096, 0.125, 0.192, 0.25, 0.384, 0.5, 0.768, 1, [2, 10.66] MSps
#       Filter bandwidths: 0.2, 0.3, 0.6, 1.536, 5, 6, 7, 8 MHz
#   sudo systemctl start sdrplay
#   sudo systemctl stop sdrplay
#   
'''
=============== bandWidthHz = 5000000.0
sdr.listSampleRates = [62500, 96000, 125000, 192000, 250000, 384000, 500000, 768000, 1000000, 2000000, 2048000, 3000000, 4000000, 5000000, 6000000, 7000000, 8000000, 9000000, 10000000]
sdr.getGain = 47.0
sdr.getFrequency = 1420405000.0
sdr.getSampleRate = 2000000.0
sdr.getBandwidth = 5000000.0
sdr.getFrequencyCorrection = 0.0
[INFO] Using format CF32.
[ERROR] error in activateStream() - Init() failed: sdrplay_api_Fail




=============== bandWidthHz = 5000000.0
sdr.listSampleRates = [62500, 96000, 125000, 192000, 250000, 384000, 500000, 768000, 1000000, 2000000, 2048000, 3000000, 4000000, 5000000, 6000000, 7000000, 8000000, 9000000, 10000000]
sdr.getGain = 47.0
sdr.getFrequency = 1420405000.0
sdr.getSampleRate = 2000000.0
sdr.getBandwidth = 5000000.0
sdr.getFrequencyCorrection = 0.0
[INFO] Using format CF32.
[ERROR] error in activateStream() - Init() failed: sdrplay_api_Fail


'''





# ezColS250524a, Airspy sdr.writeSetting("biastee", "true")
# ezColS250521dr
# ezColS250521d
# ezColS250521c, dusting,
#   After ezRA Linux installation, I think Soapy Linux installation as on
#     https://github.com/byggemandboesen/RadioPy
#       sudo apt install swig
#       sudo apt install python3-soapysdr
#       sudo apt install soapysdr-tools
#       python3 --version
#       SoapySDRUtil --help
#       SoapySDRUtil --info
#         Available factories... airspy, audio, bladerf, hackrf, lime, miri, osmosdr, redpitaya, remote, rfspace, rtlsdr, uhd
#       SoapySDRUtil --find
#
#       a@u22-221222a:~/ezRABase/ezColS$ lsusb
#           Bus 003 Device 004: ID 1d50:60a1 OpenMoko, Inc. Airspy
#       https://github.com/pothosware/SoapyAirspy/wiki
#       SoapySDRUtil --probe=driver=airspy
#       driver = 'airspy'       # this   ezColS250521dr default 31000 in 3.0 seconds
#       https://github.com/pothosware/SoapyAirspy/issues/10
#
#       a@u22-221222a:~/ezRABase/ezColS$ lsusb
#           Bus 003 Device 002: ID 0bda:2838 Realtek Semiconductor Corp. RTL2838 DVB-T
#       https://github.com/pothosware/SoapyRTLSDR/wiki
#       https://github.com/Nuand/bladeRF/issues/757
#       sudo apt install rtl-sdr librtlsdr-dev
#       SoapySDRUtil --probe=driver=rtlsdr
#       SoapySDRUtil --probe=driver=rtlsdr      # this   ezColS250521d default 31000 in 4.4 seconds
#       driver = 'rtlsdr'       # this   ezColS250521dr default 31000 in 4.4 seconds
#                               # posted ezCol250313a   default 31000 in 5.1 seconds
#
#       https://github.com/pothosware/SoapySDRPlay3/wiki
#       a@u22-221222a:~/ezRABase/ezColS$ lsusb
#           Bus 003 Device 018: ID 1df7:3010 SDRplay RSP2/RSP2pro
#       a@u22-221222a:~/ezRABase/ezColS$ lsusb -d 1df7:
#           Bus 003 Device 018: ID 1df7:3010 SDRplay RSP2/RSP2pro


'''
a@u22-221222a:~/Downloads/SoapySDRPlay3/build$ sudo make install
[ 16%] Building CXX object CMakeFiles/sdrPlaySupport.dir/Registration.cpp.o
/home/a/Downloads/SoapySDRPlay3/Registration.cpp: In function ‘std::vector<std::map<std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > > findSDRPlay(const Kwargs&)’:
/home/a/Downloads/SoapySDRPlay3/Registration.cpp:52:26: error: ‘struct sdrplay_api_DeviceT’ has no member named ‘valid’
   52 |       if (not rspDevs[i].valid) continue;
      |                          ^~~~~
/home/a/Downloads/SoapySDRPlay3/Registration.cpp:66:36: error: ‘SDRPLAY_RSP1B_ID’ was not declared in this scope; did you mean ‘SDRPLAY_RSP1A_ID’?
   66 |       else if (rspDevs[i].hwVer == SDRPLAY_RSP1B_ID)
      |                                    ^~~~~~~~~~~~~~~~
      |                                    SDRPLAY_RSP1A_ID
/home/a/Downloads/SoapySDRPlay3/Registration.cpp:78:36: error: ‘SDRPLAY_RSPdxR2_ID’ was not declared in this scope; did you mean ‘SDRPLAY_RSPdx_ID’?
   78 |       else if (rspDevs[i].hwVer == SDRPLAY_RSPdxR2_ID)
      |                                    ^~~~~~~~~~~~~~~~~~
      |                                    SDRPLAY_RSPdx_ID
make[2]: *** [CMakeFiles/sdrPlaySupport.dir/build.make:76: CMakeFiles/sdrPlaySupport.dir/Registration.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:84: CMakeFiles/sdrPlaySupport.dir/all] Error 2
make: *** [Makefile:136: all] Error 2
a@u22-221222a:~/Downloads/SoapySDRPlay3/build$ 


https://www.google.com/search?q=isvalid()+c%2B%2B&oq=cpp+.valid
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute.isvalid?view=net-9.0



#       SoapySDRUtil --probe=driver=sdrplay
https://kb9rlw.blogspot.com/2021/06/a-simple-script-to-get-sdrplay-hardware.html
'''


#       sudo apt install sdrplay libsdrplay-dev





# ezColS250521b, dusting
# ezColS250521a, works?, but many overflow and [INFO] output to standard error stream (stderr)
#   ('std::cerr' in https://github.com/osmocom/gr-osmosdr/blob/master/lib/airspy/airspy_source_c.cc)
# ezColS250520d
# ezColS250520c, working ?
# ezColS250520a, after study using ezColS250516c250520b.py got some data,
#   after flattening readFromStream()
# ezColS250519c, collect soapy control into sdrTask(), bandwidth problem ?
# ezColS250519b
#   https://github.com/pothosware/SoapySDR/wiki/PythonSupport
#   https://github.com/pothosware/SoapySDR/tree/master/swig/python/apps
# ezColS250519a first using https://github.com/byggemandboesen/RadioPy code
# ezColS250516c
# ezColS250516a, ColS for soapysdr
#   https://wiki.gnuradio.org/index.php/Soapy
#   SoapySDRUtil --info
#     Available factories... airspy, audio, bladerf, hackrf, lime, miri, osmosdr, redpitaya, remote, rfspace, rtlsdr, uhd
#   SoapySDRUtil --find
#     driver = rtlsdr
#     and indicates if physically plugged in
#   SoapySDRUtil --probe=driver=rtlsdr
#   === physically replaced the rtlsdr SDR with an AirSpy R2 SDR
#   SoapySDRUtil --find
#     driver = airspy
#   SoapySDRUtil --probe=driver=airspy
#   SoapySDRUtil --make=driver=airspy
#   a@u22-221222a:~/ezRABase/ezColAS$ python3 --version
#     Python 3.10.12
#   https://github.com/pothosware/SoapySDR/issues/272
#   https://github.com/pothosware/SoapySDR/wiki
#   https://github.com/pothosware/SoapyAirspy/wiki
#   https://github.com/pothosware/PothosCore/wiki/Ubuntu
#     The PPAs support the following Ubuntu releases:
#       Focal (20.04 LTS)
#   a@u22-221222a:~/ezRABase/ezColAS$ uname -a
#     ... 22.04.1-Ubuntu ...
#   a@u22-221222a:~/ezRABase/ezColAS$ python3  ../ezRA/ezColS250516a.py -ezColIntegQty 1000
#     works at a slow 13.6 seconds per sample

# ezColAS240522a, Airspy R2, first sampleNumber and fileSample are now 0
# ezColAS240521c, Airspy R2, getting slow samples ??
# ezColAS240521b, Airspy R2 ? - but still many problems
#   combine Airspy soapysdr code from ezColAS240521a and ezColAS240118a into modern ezCol240422a,
#   now Soapy supports my Ubuntu 22.04.4 LTS (Jammy Jellyfish) ?
#       https://releases.ubuntu.com/
#           jammy/      2024-02-22 15:39    -   Ubuntu 22.04.4 LTS (Jammy Jellyfish)
#   https://ubuntu.pkgs.org/22.04/ubuntu-universe-arm64/soapysdr-module-all_0.8.1-2build1_arm64.deb.html
#       sudo apt-get update
#       sudo apt-get install soapysdr-module-all
#           240521 and now does much more ???
#   https://groups.google.com/g/sara-list/c/SH5n2PbKOGo
#       Marcus D. Leech says
#       https://github.com/pothosware/SoapySDR/wiki/PythonSupport

# ezCol250228a, -ezColRefAction
# ezCol250219a, -ezColSecMax
# ezCol250212a, -ezColSecMax
# ezCol240716a, helpful config comments for ezSerRelay.py
# ezCol240715b, swapped locations of 'To File' and 'Off' RadioButtons
# ezCol240715a, Python3.12+ SyntaxWarnings
# ezCol240714b, relayQuietS for Linux syntax
# ezCol240714a, removed several os.path.sep,
#   improvements thanks to Richard Cochois
#       ezColGain to float, and sdr.set_gain(), and sdr.get_gain(),
#       sdr.valid_gains_db,
#       ezColBiasTeeOn,
#       relaySudoS and relayQuietS,
#       ezColUsbRelay == 15 on Linux, using new ezSerRelay.py
#       sdr.close()
# ezCol240712a, raw string for -ezDefaultsFile Windows help
# ezCol240706a, use double quotes to allow for spaces in path of Window call of hidusb-relay-cmd.exe
# ezCol240705a, accounting for any spaces in path, '\ ' to r'\ ' to fix the many (Python3.12+ ?)
#       SyntaxWarning: invalid escape sequence '\ ',
#   moved print of sys.version
# ezCol240522a, first sampleNumber and fileSample are now 0
# ezCol240422a, newFileButton should not clear stripchart plots
# ezCol240420a, fixed New File,
#   many improvements near rmsAvgHistoryLenRecent24Hour and rmsAvgHistoryLenRecent1Hour
# ezCol240419b, New File bug
# ezCol240419a, fixes for ezColDashboard 0, stopped at rmsAvgHistoryLenRecentMost,
#   sys.version
# ezCol240418a, stops at 510 ?, if not ezColDashboard: programStateQueue = Queue()
# ezCol240417f, dusting
# ezCol240417e, unlock graphics window autofocus (allows window sizing), dusting
# ezCol240417c, plot trimmed rmsAvgHistory to avoid y-autoScaling on unseen data,
#   unlock graphics window autofocus ?
# ezCol240417b, if "Idle", print red fileNameDashS inside parentheses
# ezCol240417a,
#   0/1/2 = Collect/Idle/Exit to "To File/Idle/Exit"
# ezCol240416a, -ezColSampleMax for environment RFI surveys
#   0/1/2 = Collect/Pause/Exit to Collect/Idle/Exit
#       where Idle still exercises the SDR to keep it warm,
#   removed need for programStatePutLast and ezColIntegQtyPutLast
# ezCol240221b, add RaDec and GLatGLon creation,
#   changed ezRA .txt file format
#       from 'az x.xx el y.yy' to 'azDeg x.xx elDeg y.yy',
#       and from (unposted) 'ra x.xx dec y.yy' to 'raH x.xx decDeg y.yy',
#       and from (unposted) 'glat x.xx glon y.yy' to 'gLatDeg x.xx gLonDeg y.yy',
#       so will need updates to ezCon240219b.py and ezColGNSM240219d.py
# ezCol240221a, add RaDec and GLatGLon creation
# ezCol231211a.py, if ezColUsbRelay, add ' Relay' or ' R' to end of ezColCenterFreqRef display lines
# ezCol231108a.py, ezColAntBtwnRef into ezColArgumentsFile()
# ezCol231004c.py, write commandStringEnd in file header
# ezCol231004b.py, write commandString in file header
# ezCol231004a.py, write commandString in file header
# ezCol230406a.py, -eX
# ezCol230316a.py, help screen edits, -eX
# ezCol230315a.py, no ezColDashboard needed centerFreqRefHz, major ezColUsbRelay improvements
# ezCol230305a.py, boilerplate from ezSky
# ezCol230301a.py, -ezez help wording
# ezCol230228a.py, ezCol230208a.py did programStateQueue.put() and ezColIntegQtyQueue.put() way too often
# ezCol230208a.py, REF should be defined in data collection process,
#   moved REF and relay control into sdrTask, ezColIntegQtyQueue, now better Ref data
# ezCol230207c.py, marked REF one sample too early, so write with dataFlagsSLast - worked but first REF is second sample, want as first
# ezCol230207b.py, marked REF one sample too early, so write with dataFlagsSLast
# ezCol230207a.py, marked REF one sample too early, ignore first sample - but no effect
# ezCol230206a.py, name change, seems successful, so I now retire experimental ezColX name.
#   Last single-process version was ezCol221110a.py .
#   Now latest ezColX230205c becomes non-experimental ezCol230206a and ezCol.
# ezColX230205c.py, SampleQty and flags show sample currently being created, had serialsend.exe commands reversed
# ezColX230205b.py, cont.
# ezColX230205a.py, name change
# ezColXP230204a.py, more defining detail to -ezColUsbRelay 1 2 and 3,
# ezColXP230203a.py, Pablo code confused by space in dir name, want hidusb-relay-cmd.exe and serialSend.exe found in ezColX directory,
# ezColXP221220a.py, Pablo USB Relay control serialSend.exe experiment
# ezColXP221219a.py, Pablo USB Relay control serialSend.exe experiment
# ezColX221202a.py, renamed for GitHub
# ezCol221110atbb.py, ezColIntegQty needed by sdrTask(), reordered sdrTask() arguments,
#   small fixes, program overview
# ezCol221110atba.py, https://www.youtube.com/watch?v=AZnGRKFUU0c at 0:36
#   says Python multithreading is still one process,
#   so, changed 2 threads to 2 processes, rearranged blocks
# ezCol221110atb.py, second threaded ezCol
# ezCol221110a.py, changed Linux from double SPDT "BITFT" USB Relay to single SPDT "HW348" USB Relay
# ezCol220930a.py, prep for Git
# ezCol220826a.py, dataFlagsS if no REF


#########################################################################################
# ezCol program overview:

# I think this ezCol line to read the SDR hardware is what slows the program:
#    psd = np.abs(np.fft.fft(sdr.read_samples(freqBinQty)))
# I think this command on that line is what slows the program:
#    sdr.read_samples(freqBinQty)
# That leaves too little time to update and read the dashboard graphics window.

# After initialization, that is why 2 processes: main process loop, and sdrTask process loop.
# ezCol program assumes sdrTask process loop takes more time than main process loop.

# main process:
#    initialize: help screen, read in arguments
#    programState = 0 (0: Collect, 1: Pause, 2: Exit)
#    if ezColDashboard:
#        initialize dashboard (including buttons and programState)
#    create new 'data' directory, unless already exists
#    initialize and start the SDR Process 'sdrTask'
#    initialize while loop
#    while programState <= 1: (0: Collect, 1: Pause, 2: Exit)
#        get sdrOutQueue
#        if sdrOutQueue had New Data:
#            initialize timestamp
#            set centerFreq
#            maybe start new data file (if new UTC day, or if newFileButton)
#            maybe write an az line (if az or el changed)
#            write data sample line
#            if ezColVerbose:
#                print status
#            if ezColDashboard:
#                erase dashboard
#                plot top right text section
#                plot 3 stripcharts
#                plot top left frequency spectrum
#            remember timestamp
#        allow time for dashboard interaction

# sdrTask process:
#    initialize feedRef relay, if any
#    while programState <= 1: (0: Collect, 1: Pause, 2: Exit)
#        get the programStateQueue
#        if programState == 2:
#            exit
#        if programState == 0 (not Paused):
#            set feedRef relay
#            set the center frequency
#            create and put tuple with Power Spectral Density (PSD), values averaged
#              from ezColIntegQty datasets
#########################################################################################

import os
import sys
import time
import numpy as np
# delayed other imports until after possible help screen

from SoapySDR import * #SOAPY_SDR_ constants



def printHello():

    global programRevision          # string
    global commandString            # string

    print()
    print('         For help, run')
    print('            ezCol.py -help')

    print()
    print('=================================================')
    print(' Local time =', time.asctime(time.localtime()))
    print(' programRevision =', programRevision)
    print(' Python sys.version =', sys.version)
    print()

    commandString = '  '.join(sys.argv)
    print(' This Python command = ' + commandString)



def printUsage():

    print()
    print()
    print('##############################################################################################')
    print()
    print('USAGE:')
    print('  Windows:   py      ezCol.py [optional arguments]')
    print('  Linux:     python3 ezCol.py [optional arguments]')
    print()
    print('  Easy Radio Astronomy (ezRA) ezCol data Collector')
    print('  to record radio to "data" directory frequency spectrum data ezRA .txt files.')
    print('  ezCol will create a "data" directory if necessary.')
    print()
    print('  Arguments are read first from inside the ezCol program,')
    print("  then in order from the ezDefaults.txt file in the ezCol.py's directory,")
    print('  then in order from the ezDefaults.txt file in current directory,')
    print('  then in order from the command line.  For duplicates, last read wins.')
    print()
    print('EXAMPLES:')
    print()
    print('  py ezCol.py -help                         (Print this help)')
    print('  py ezCol.py -h                            (Print this help)')
    print()
    print('              -ezRAObsLat    40.2           (Observatory Latitude  (degrees))')
    print('              -ezRAObsLon  -105.1           (Observatory Longitude (degrees))')
    print('              -ezRAObsAmsl 1524.0           (Observatory Above Mean Sea Level (meters))')
    print('              -ezRAObsName bigDish8         (Observatory Name)')
    print('              -ezColFileNamePrefix bigDish8 (Data File Name Prefix)')
    print()
    print('              -ezColCenterFreqAnt 1420.405  (Signal          Frequency  (MHz))')
    print('              -ezColCenterFreqRef 1423.405  (Dicke Reference Frequency  (MHz))')
    print('              -ezColBandWidth        2.400  (Signal          Bandwidth  (MHz))')
    print('              -ezColSampleRate       2.400  (Signal          SampleRate (MHz))')
    print()
    print('              -ezColReadBinQtyBits   8      (For  256 Reading freqBin Qty frequencies)')
    print('              -ezColReadBinQtyBits   9      (For  512 Reading freqBin Qty frequencies)')
    print('              -ezColReadBinQtyBits  10      (For 1024 Reading freqBin Qty frequencies)')
    print('              -ezColReadBinQtyBits  11      (For 2048 Reading freqBin Qty frequencies)')
    print('              -ezColReadBinQtyBits  12      (For 4096 Reading freqBin Qty frequencies)')
    print('              -ezColReadBinMin      1024    (Lowest Reading freqBin of the freqBinQty recorded)')
    print()
    print('              -ezColFreqBinQtyBits   8      (For  256 freqBinQty recorded frequencies)')
    print('              -ezColFreqBinQtyBits   9      (For  512 freqBinQty recorded frequencies)')
    print('              -ezColFreqBinQtyBits  10      (For 1024 freqBinQty recorded frequencies)')
    print()
    print('              -ezColGain        999.9       (Use max gain)')
    #print('              -ezColOffsetPPM   5           (Tuner offset Parts-Per-Million (integer)')
    print('              -ezColAntBtwnRef  5           (number of Ant samples between Ref samples)')
    #print()
    #print('              -ezColBiasTeeOn   -1          (SDR BiasTee voltage no change)')
    #print('              -ezColBiasTeeOn    0          (SDR BiasTee voltage OFF)')
    #print('              -ezColBiasTeeOn    1          (SDR BiasTee voltage ON )')
    print()
    #print('              -ezColAzimuth   180.0         (Azimuth   pointing of antenna (degrees))')
    #print('              -ezColElevation  45.0         (Elevation pointing of antenna (degrees))')
    print('              -ezColAzDeg     180.0         (Azimuth            pointing of antenna (degrees))')
    print('              -ezColElDeg      45.0         (Elevation          pointing of antenna (degrees))')
    print()
    print('              -ezColRaH        20.0         (Right Ascension    pointing of antenna (hours))')
    print('              -ezColDecDeg     40.7         (Declination        pointing of antenna (degrees))')
    print()
    print('              -ezColGLatDeg     0.0         (Galactic Latitude  pointing of antenna (degrees))')
    print('              -ezColGLonDeg    30.0         (Galactic Longitude pointing of antenna (degrees))')
    print()
    print('              -ezColVerbose     1           (Turn on Verbose mode)')
    print('              -ezColDashboard   0           (Turn off graphical display)')
    print('              -ezColDispGrid    1           (Turn on graphical display plot grids)')
    print()
    #print('              -ezColUsbRelay   0            (No relays driving a feed Dicke reference)')
    #print('              -ezColUsbRelay   1            (1 SPST HID relay, driving feedRef ON or OFF)')
    #print('              -ezColUsbRelay   2            (2 SPST HID relays, driving a latching feedRef',
    #    ' relay with pulses)')
    #print('              -ezColUsbRelay   3            (1 SPST non-HID relay, driving feedRef ON or OFF)')
    print('              -ezColUsbRelay    0           (No relays driving a feed Dicke reference)')
    print('              -ezColUsbRelay   11           (1 SPST HID relay,     driving feedRef ON or OFF)')
    print('              -ezColUsbRelay   15           (1 SPST non-HID relay, driving feedRef ON or OFF)')
    print('              -ezColUsbRelay   21           (2 SPST HID relays, #1 driving feedRef ON or OFF)')
    print('              -ezColUsbRelay   22           (2 SPST HID relays, #2 driving feedRef ON or OFF)')
    print('              -ezColUsbRelay   29           (2 SPST HID relays, driving a latching feedRef',
        'relay with pulses)')
    print()
    print('              -ezColIntegQty   31000        (Number of readings to be integrated into',
        'one sample)')
    print('              -ezColTextFontSize   11       (Size of text font)')
    print('              -ezColSampleMax      10       (last sample number before exit)')
    print('              -ezColSecMax         3600.2   (maximum number of seconds before exit)')
    print('              -ezColRefAction  1            (0/1/2 for initial Off/RefDiv/RefSub)')
    print('              -ezColYLimL      0.1  0.4     (Fraction of Y Auto Scale, Min and Max)')
    print()
    print(r'              -ezDefaultsFile ..\bigDish8.txt   (Additional file of default arguments)')
    print()
    print('              -eXXXXXXXXXXXXXXzIgonoreThisWholeOneWord')
    print('         (any one word starting with -eX is ignored, handy for long command line editing)')
    print()
    print()
    print(' programRevision =', programRevision)
    print()
    print()
    print()
    print()
    print()
    print('       The Society of Amateur Radio Astronomers (SARA)')
    print('                    radio-astronomy.org')
    print()
    print()
    print()
    print()
    print()
    print('##############################################################################################')
    print()



    '''
    # https://github.com/pothosware/SoapySDR/wiki/PythonSupport
    import SoapySDR
    #from SoapySDR import * #SOAPY_SDR_ constants
    from SoapySDR import Device
    #import numpy #use numpy for buffers

    #enumerate devices
    results = SoapySDR.Device.enumerate()
    for result in results: print(result)

    #create device instance
    #args can be user defined or from the enumeration result
    #args = dict(driver="rtlsdr")
    args = dict(driver="airspy")
    sdr = SoapySDR.Device(args)

    #query device info
    print('SOAPY_SDR_RX =', SOAPY_SDR_RX)
    print('SOAPY_SDR_CF32 =', SOAPY_SDR_CF32)
    print(sdr.listAntennas(SOAPY_SDR_RX, rx_chan))
    print(sdr.listGains(SOAPY_SDR_RX, rx_chan))
    freqs = sdr.getFrequencyRange(SOAPY_SDR_RX, rx_chan)
    for freqRange in freqs: print(freqRange)

    #apply settings
    sdr.setSampleRate(SOAPY_SDR_RX, rx_chan, 1e6)
    sdr.setFrequency(SOAPY_SDR_RX, rx_chan, 912.3e6)

    #setup a stream (complex floats)
    rxStream = sdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CF32)
    sdr.activateStream(rxStream) #start streaming

    #create a re-usable buffer for rx samples
    buff = np.array([0]*1024, np.complex64)

    #receive some samples
    for i in range(10):
        sr = sdr.readStream(rxStream, [buff], len(buff))
        print(sr.ret) #num samples or error code
        print(sr.flags) #flags set by receive operation
        print(sr.timeNs) #timestamp for receive buffer

    #shutdown the stream
    sdr.deactivateStream(rxStream) #stop streaming
    sdr.closeStream(rxStream)
    '''

    sys.exit()



def ezColArgumentsFile(ezDefaultsFileNameInput):
    # process arguments from file

    global ezRAObsLat                       # float
    global ezRAObsLon                       # float
    global ezRAObsAmsl                      # float
    global ezRAObsName                      # string
    global ezColFileNamePrefix              # string

    global ezColCenterFreqAnt               # float
    global ezColCenterFreqRef               # float
    global ezColBandWidth                   # float
    global ezColSampleRate                  # float

    global ezColReadBinQtyBits              # integer
    global ezColReadBinMin                  # integer
    global ezColFreqBinQtyBits              # integer
    global ezColGain                        # float
    global ezColBiasTeeOn                   # integer
    global ezColAntBtwnRef                  # integer

    global ezColAzDeg                       # float
    global ezColElDeg                       # float
    global ezColRaH                         # float
    global ezColDecDeg                      # float
    global ezColGLatDeg                     # float
    global ezColGLonDeg                     # float
    global coordType                        # integer

    global ezColVerbose                     # integer
    global ezColDashboard                   # integer
    global ezColDispGrid                    # integer

    global ezColUsbRelay                    # integer

    global ezColIntegQty                    # integer
    global ezColTextFontSize                # integer
    global ezColSampleMax                   # integer
    global ezColSecMax                      # float
    global ezColRefAction                   # integer
    global ezColYLim0                       # float
    global ezColYLim1                       # float


    print()
    print('   ezColArgumentsFile(' + ezDefaultsFileNameInput + ') ===============')

    # https://www.zframez.com/tutorials/python-exception-handling.html
    try:
        fileDefaults = open(ezDefaultsFileNameInput, 'r')

        print('      success opening ' + ezDefaultsFileNameInput)

        while 1:
            fileLine = fileDefaults.readline()

            # LF always present: 0=EOF  1=LF  2=1Character
            if len(fileLine) < 1:         # if end of file
                break                     # get out of while loop

            thisLineSplit = fileLine.split()
            if len(thisLineSplit) < 1:         # if line all whitespace
                continue                  # skip to next line

            if thisLineSplit[0][0] == '#':    # ignoring whitespace, if first character of first word
                continue                  # it is a comment, skip to next line


            # be kind, ignore argument keyword capitalization
            thisLine0Lower = thisLineSplit[0].lower()

            # ezRA family arguments
            if thisLine0Lower == '-ezRAObsName'.lower():
                ezRAObsName = thisLineSplit[1]

            elif thisLine0Lower == '-ezRAObsLat'.lower():
                ezRAObsLat  = float(thisLineSplit[1])

            elif thisLine0Lower == '-ezRAObsLon'.lower():
                ezRAObsLon  = float(thisLineSplit[1])

            elif thisLine0Lower == '-ezRAObsAmsl'.lower():
                ezRAObsAmsl = float(thisLineSplit[1])

            elif thisLine0Lower == '-ezColFileNamePrefix'.lower():
                ezColFileNamePrefix = thisLineSplit[1]


            # ezCol arguments
            # integer arguments
            elif thisLine0Lower == '-ezColReadBinQtyBits'.lower():
                ezColReadBinQtyBits = int(thisLineSplit[1])

            elif thisLine0Lower == '-ezColReadBinMin'.lower():
                ezColReadBinMin = int(float(thisLineSplit[1]))

            elif thisLine0Lower == '-ezColFreqBinQtyBits'.lower():
                ezColFreqBinQtyBits = int(thisLineSplit[1])

            elif thisLine0Lower == '-ezColBiasTeeOn'.lower():
                ezColBiasTeeOn = int(thisLineSplit[1])

            elif thisLine0Lower == '-ezColAntBtwnRef'.lower():
                ezColAntBtwnRef = int(thisLineSplit[1])

            elif thisLine0Lower == '-ezColVerbose'.lower():
                ezColVerbose = int(thisLineSplit[1])

            elif thisLine0Lower == '-ezColDashboard'.lower():
                ezColDashboard = int(thisLineSplit[1])

            elif thisLine0Lower == '-ezColDispGrid'.lower():
                ezColDispGrid = int(thisLineSplit[1])

            elif thisLine0Lower == '-ezColUsbRelay'.lower():
                ezColUsbRelay = int(thisLineSplit[1])

            elif thisLine0Lower == '-ezColIntegQty'.lower():
                ezColIntegQty = int(float(thisLineSplit[1]))

            elif thisLine0Lower == '-ezColTextFontSize'.lower():
                ezColTextFontSize = int(thisLineSplit[1])

            elif thisLine0Lower == '-ezColSampleMax'.lower():
                ezColSampleMax = float(thisLineSplit[1])

            elif thisLine0Lower == '-ezColSecMax'.lower():
                ezColSecMax = float(thisLineSplit[1])

            elif thisLine0Lower == '-ezColRefAction'.lower():
                ezColRefAction = int(thisLineSplit[1])


            # float arguments
            elif thisLine0Lower == '-ezColCenterFreqAnt'.lower():
                ezColCenterFreqAnt = float(thisLineSplit[1])

            elif thisLine0Lower == '-ezColCenterFreqRef'.lower():
                ezColCenterFreqRef = float(thisLineSplit[1])

            elif thisLine0Lower == '-ezColBandWidth'.lower():
                ezColBandWidth = float(thisLineSplit[1])

            elif thisLine0Lower == '-ezColSampleRate'.lower():
                ezColSampleRate = float(thisLineSplit[1])

            elif thisLine0Lower == '-ezColAzimuth'.lower() or thisLine0Lower == '-ezColAzDeg'.lower():
                ezColAzDeg = float(thisLineSplit[1])
                coordType = 0               # AzEl

            elif thisLine0Lower == '-ezColElevation'.lower() or thisLine0Lower == '-ezColElDeg'.lower():
                ezColElDeg = float(thisLineSplit[1])
                coordType = 0               # AzEl

            elif thisLine0Lower == '-ezColRaH'.lower():
                ezColRaH = float(thisLineSplit[1])
                coordType = 1               # RaDec

            elif thisLine0Lower == '-ezColDecDeg'.lower():
                ezColDecDeg = float(thisLineSplit[1])
                coordType = 1               # RaDec

            elif thisLine0Lower == '-ezColGLatDeg'.lower():
                ezColGLatDeg = float(thisLineSplit[1])
                coordType = 2               # GLatGLon

            elif thisLine0Lower == '-ezColGLonDeg'.lower():
                ezColGLonDeg = float(thisLineSplit[1])
                coordType = 2               # GLatGLon

            #elif thisLine0Lower == '-ezColOffsetPPM'.lower():
            #    ezColOffsetPPM = int(thisLineSplit[1])

            elif thisLine0Lower == '-ezColGain'.lower():
                ezColGain = float(thisLineSplit[1])


            # list arguments
            elif thisLine0Lower == '-ezColYLimL'.lower():
                ezColYLim0 = float(thisLineSplit[1])
                ezColYLim1 = float(thisLineSplit[2])

            elif thisLine0Lower[:6] == '-ezCol'.lower():
                print()
                print()
                print()
                print()
                print()
                print(' ========== FATAL ERROR:  Defaults file ( ' + ezDefaultsFileNameInput + ' )')
                print(" has this line's unrecognized first word:")
                print(fileLine)
                print()
                print()
                print()
                print()
                print('\a')     # ring bell
                sys.exit()

            else:
                pass    # unrecognized first word, but no error, allows for other ezRA programs

    except (FileNotFoundError, IOError):
    	pass
    else:
        fileDefaults.close()       #   then have processed all available lines in this defaults file



def ezColArgumentsCommandLine():
    # process arguments from command line

    global commandString                    # string

    global ezRAObsLat                       # float
    global ezRAObsLon                       # float
    global ezRAObsAmsl                      # float
    global ezRAObsName                      # string
    global ezColFileNamePrefix              # string

    global ezColCenterFreqAnt               # float
    global ezColCenterFreqRef               # float
    global ezColBandWidth                   # float
    global ezColSampleRate                  # float

    global ezColReadBinQtyBits              # integer
    global ezColReadBinMin                  # integer
    global ezColFreqBinQtyBits              # integer
    global ezColGain                        # float
    global ezColBiasTeeOn                   # integer
    global ezColAntBtwnRef                  # integer

    global ezColAzDeg                       # float
    global ezColElDeg                       # float
    global ezColRaH                         # float
    global ezColDecDeg                      # float
    global ezColGLatDeg                     # float
    global ezColGLonDeg                     # float
    global coordType                        # integer

    global ezColVerbose                     # integer
    global ezColDashboard                   # integer
    global ezColDispGrid                    # integer

    global cmdDirectoryS                    # string            creation

    global ezColUsbRelay                    # integer

    global ezColIntegQty                    # integer
    global ezColTextFontSize                # integer
    global ezColSampleMax                   # integer
    global ezColSecMax                      # float
    global ezColRefAction                   # integer
    global ezColYLim0                       # float
    global ezColYLim1                       # float


    print()
    print('   ezColArgumentsCommandLine ===============')

    cmdLineSplit = commandString.split()
    cmdLineSplitLen = len(cmdLineSplit)

    cmdLineSplitIndex = 1
    cmdDirectoryS = ''

    while cmdLineSplitIndex < cmdLineSplitLen:

        # be kind, ignore argument keyword capitalization
        cmdLineArgLower = cmdLineSplit[cmdLineSplitIndex].lower()


        if cmdLineArgLower == '-help':
            printUsage()                    # will sys.exit()

        elif cmdLineArgLower == '--help':
            printUsage()                    # will sys.exit()

        elif cmdLineArgLower == '-h':
            printUsage()                    # will sys.exit()

        elif cmdLineArgLower == '--h':
            printUsage()                    # will sys.exit()


        elif 2 <= len(cmdLineArgLower) and cmdLineArgLower[:2] == '-e':

            # Ignoring whitespace, first characters of cmdLineSplit word are '-e'.
            # Not a data directory or file.

            # ezRA arguments used by multiple programs:
            if cmdLineArgLower == '-ezRAObsLat'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezRAObsLat  = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezRAObsLon'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezRAObsLon  = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezRAObsAmsl'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezRAObsAmsl = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezRAObsName'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezRAObsName = cmdLineSplit[cmdLineSplitIndex]   # cmd line allows only one ezRAObsName word

            elif cmdLineArgLower == '-ezColFileNamePrefix'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColFileNamePrefix = cmdLineSplit[cmdLineSplitIndex]


            # integer arguments:
            elif cmdLineArgLower == '-ezColReadBinQtyBits'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColReadBinQtyBits = int(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColReadBinMin'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColReadBinMin = int(float(cmdLineSplit[cmdLineSplitIndex]))

            elif cmdLineArgLower == '-ezColFreqBinQtyBits'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColFreqBinQtyBits = int(cmdLineSplit[cmdLineSplitIndex])

            #elif cmdLineArgLower == '-ezColOffsetPPM'.lower():
            #    cmdLineSplitIndex += 1      # point to first argument value
            #    ezColOffsetPPM = int(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColGain'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColGain = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColBiasTeeOn'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColBiasTeeOn = int(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColAntBtwnRef'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColAntBtwnRef = int(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColVerbose'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColVerbose = int(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColDashboard'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColDashboard = int(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColDispGrid'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColDispGrid = int(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColUsbRelay'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColUsbRelay = int(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColIntegQty'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColIntegQty = int(float(cmdLineSplit[cmdLineSplitIndex]))

            elif cmdLineArgLower == '-ezColTextFontSize'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColTextFontSize = int(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColSampleMax'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColSampleMax = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColSecMax'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColSecMax = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColRefAction'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColRefAction = int(cmdLineSplit[cmdLineSplitIndex])


            # float arguments:
            elif cmdLineArgLower == '-ezColCenterFreqAnt'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColCenterFreqAnt = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColCenterFreqRef'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColCenterFreqRef = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColBandWidth'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColBandWidth = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColSampleRate'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColSampleRate = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezColAzimuth'.lower() or cmdLineArgLower == '-ezColAzDeg'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColAzDeg = float(cmdLineSplit[cmdLineSplitIndex])
                coordType = 0               # AzEl

            elif cmdLineArgLower == '-ezColElevation'.lower() or cmdLineArgLower == '-ezColElDeg'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColElDeg = float(cmdLineSplit[cmdLineSplitIndex])
                coordType = 0               # AzEl

            elif cmdLineArgLower == '-ezColRaH'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColRaH = float(cmdLineSplit[cmdLineSplitIndex])
                coordType = 1               # RaDec

            elif cmdLineArgLower == '-ezColDecDeg'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColDecDeg = float(cmdLineSplit[cmdLineSplitIndex])
                coordType = 1               # RaDec

            elif cmdLineArgLower == '-ezColGLatDeg'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColGLatDeg = float(cmdLineSplit[cmdLineSplitIndex])
                coordType = 2               # GLatGLon

            elif cmdLineArgLower == '-ezColGLonDeg'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColGLonDeg = float(cmdLineSplit[cmdLineSplitIndex])
                coordType = 2               # GLatGLon


            # list arguments:
            elif cmdLineArgLower == '-ezColYLimL'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColYLim0 = float(cmdLineSplit[cmdLineSplitIndex])
                cmdLineSplitIndex += 1
                ezColYLim1 = float(cmdLineSplit[cmdLineSplitIndex])

            elif cmdLineArgLower == '-ezDefaultsFile'.lower():
                cmdLineSplitIndex += 1      # point to first argument value
                ezColArgumentsFile(cmdLineSplit[cmdLineSplitIndex])

            # ignore silly -eX* arguments, for handy neutralization of command line arguments,
            #   but remove spaces before argument numbers
            #   (can not use '-x' which is a preface to a negative hexadecimal number)
            elif 3 <= len(cmdLineArgLower) and cmdLineArgLower[:3] == '-ex':
                pass

            # before -eX, old spelling:
            # ignore silly -ezez* arguments, for handy neutralization of command line arguments,
            #   but remove spaces before argument numbers
            elif 5 <= len(cmdLineArgLower) and cmdLineArgLower[:5] == '-ezez':
                pass

            else:
                print()
                print()
                print()
                print()
                print()
                print(' ========== FATAL ERROR:  Command line has this unrecognized word:')
                print(cmdLineSplit[cmdLineSplitIndex])
                print()
                print()
                print()
                print()
                print('\a')     # ring bell
                sys.exit()

        cmdLineSplitIndex += 1



def ezColArguments():
    # argument: "(Computing) a value or address passed to a procedure or function at the time of call"

    global ezRAObsLat                       # float
    global ezRAObsLon                       # float
    global ezRAObsAmsl                      # float
    global ezRAObsName                      # string
    global ezColFileNamePrefix              # string

    global ezColCenterFreqAnt               # float
    global ezColCenterFreqRef               # float
    global ezColBandWidth                   # float
    global ezColSampleRate                  # float

    global ezColReadBinQtyBits              # integer
    global ezColReadBinMin                  # integer
    global ezColFreqBinQtyBits              # integer
    global ezColGain                        # float
    global ezColBiasTeeOn                   # integer
    global ezColAntBtwnRef                  # integer

    global ezColAzDeg                       # float
    global ezColElDeg                       # float
    global ezColRaH                         # float
    global ezColDecDeg                      # float
    global ezColGLatDeg                     # float
    global ezColGLonDeg                     # float
    global coordType                        # integer
    global coord0                           # float
    global coord1                           # float
    global coordMayBeNew                    # integer

    global ezColVerbose                     # integer
    global ezColDashboard                   # integer
    global ezColDispGrid                    # integer

    global ezColUsbRelay                    # integer

    global ezColIntegQty                    # integer       creation
    global ezColTextFontSize                # integer       creation
    global ezColSampleMax                   # float         creation
    global ezColSecMax                      # float         creation

    global ezColRefAction                   # integer
    global ezColYLim0                       # float         creation
    global ezColYLim1                       # float         creation


    # defaults
    if 1:
        ezRAObsName = 'defaultKS'           # Geographic Center of USA, near Lebanon, Kansas
        ezRAObsLat  =  39.8282              # Observatory Latitude  (degrees)
        ezRAObsLon  = -98.5696              # Observatory Longitude (degrees)
        ezRAObsAmsl = 563.88                # Observatory Above Mean Sea Level (meters)
        ezColFileNamePrefix = ''

        ezColCenterFreqAnt = 1420.405       # Center Frequency for Antenna signal measurements

        ezColCenterFreqRef = 0              # silly value for Center Frequency for Reference signal measurements

        # Available bandwidths
        #   3200000Hz = 3.2   MHz
        #   2800000Hz = 2.8   MHz
        #   2560000Hz = 2.56  MHz
        #   2400000Hz = 2.4   MHz = default
        #   2048000Hz = 2.048 MHz
        #   1920000Hz = 1.92  MHz
        #   1800000Hz = 1.8   MHz
        #   1400000Hz = 1.4   MHz
        #   1024000Hz = 1.024 MHz
        #    900001Hz = 0.9   MHz
        #    250000Hz = 0.25  MHz
        ezColBandWidth = 2.400              # bandwidth  (MHz), Airspy
        ezColBandWidth = 5.000              # bandwidth  (MHz), SDRplay RSP2/RSP2pro
        ezColBandWidth = 8.000              # bandwidth  (MHz), SDRplay RSP2/RSP2pro

        ezColSampleRate = 2.400             # sampleRate (MHz)

        # Each reading has ezColReadBinQtyBits Bin values.
        ezColReadBinQtyBits  =  8   # means readBinQty will be 2 to the power of  8 = 2 **  8 =  256
        #ezColReadBinQtyBits =  9   # means readBinQty will be 2 to the power of  9 = 2 **  9 =  512
        #ezColReadBinQtyBits = 10   # means readBinQty will be 2 to the power of 10 = 2 ** 10 = 1024
        #ezColReadBinQtyBits = 11   # means readBinQty will be 2 to the power of 11 = 2 ** 11 = 2048
        #ezColReadBinQtyBits = 12   # means readBinQty will be 2 to the power of 12 = 2 ** 12 = 4096

        # readBinQty SDR voltage values into the fft generate readBinQty frequency bin values.
        # Then ezColReadBinMin and freqBinQty define a subset of the whole readBinQty frequency bin values.
        # Each recorded sample starts with the ezColReadBinMin Lowest Reading freqBin of the freqBinQty values recorded.
        ezColReadBinMin = 0

        # Each recorded sample has ezColFreqBinQty Frequency Bin values.
        ezColFreqBinQtyBits  =  8   # means freqBinQty will be 2 to the power of  8 = 2 **  8 =  256
        #ezColFreqBinQtyBits =  9   # means freqBinQty will be 2 to the power of  9 = 2 **  9 =  512
        #ezColFreqBinQtyBits = 10   # means freqBinQty will be 2 to the power of 10 = 2 ** 10 = 1024

        ezColGain = 999.9           # silly big number which RtlSdr library will reduce ?
        ezColBiasTeeOn = -1         # SDR BiasTee voltage no change

        ezColAntBtwnRef = 1         # number of Ant samples between REF samples

        ezColAzDeg   = 180.0        # Azimuth            pointing of antenna (degrees)
        ezColElDeg   =  45.0        # Elevation          pointing of antenna (degrees)
        ezColRaH     =  11.3        # Right Ascension    pointing of antenna (hours)
        ezColDecDeg  =  40.0        # Declination        pointing of antenna (degrees)
        ezColGLatDeg =   0.0        # Galactic Latitude  pointing of antenna (degrees)
        ezColGLonDeg =  30.0        # Galactic Longitude pointing of antenna (degrees)
        coordType    = 0            # AzEl

        ezColVerbose   = 0
        ezColDashboard = 1
        ezColDispGrid  = 0

        ezColUsbRelay = 0           # no relays driving a feedRef

        ezColIntegQty = 31000       # number of samples to be integrated into one recorded sample
        ezColTextFontSize = 10

        ezColSampleMax = float(sys.maxsize)  # last sample number before exit
        ezColSecMax    = float(sys.maxsize)  # maximum number of seconds before exit

        ezColRefAction = 0          # default is Off
        ezColYLim0     = 0.0        # fraction of Y Auto Scale, Minimum
        ezColYLim1     = 1.0        # fraction of Y Auto Scale, Maximum

    # Program argument priority:
    #    Start with the argument value defaults inside the programs.
    #    Then replace those values with any arguments from the ezDefaults.txt in the program's directory.
    #    Then replace values with any arguments from the ezDefaults.txt in the current
    #       directory (where you are standing).
    #    Then replace values (in order) with any arguments from the command line (including
    #       any -ezDefaultsFile).
    #    Each last defined value wins.

    # process arguments from ezDefaults.txt file in the same directory as this ezCol program
    ezColArgumentsFile(os.path.dirname(__file__) + os.path.sep + 'ezDefaults.txt')

    # process arguments from ezDefaults.txt file in the current directory
    ezColArgumentsFile('ezDefaults.txt')

    # process arguments from command line
    ezColArgumentsCommandLine()

    if ezColBiasTeeOn not in [-1, 0, 1]:
        print()
        print()
        print()
        print()
        print()
        print(' ========== FATAL ERROR: ', ezColBiasTeeOn, 'is an unrecognized value for ezColBiasTeeOn')
        print()
        print()
        print()
        print()
        print('\a')     # ring bell
        sys.exit()

    # now know coordType, assign coord0, and coord1
    if coordType == 0:
        coord0     = ezColAzDeg
        coord1     = ezColElDeg
    elif coordType == 1:
        coord0     = ezColRaH
        coord1     = ezColDecDeg
    else:
        # 'GLatGLon'
        coord0     = ezColGLatDeg
        coord1     = ezColGLonDeg
    coordMayBeNew = 1

    if ezColUsbRelay not in [0, 11, 15, 21, 22, 29]:
        print()
        print()
        print()
        print()
        print()
        print(' ========== FATAL ERROR: ', ezColUsbRelay, 'is an unrecognized value for ezColUsbRelay')
        print()
        print()
        print()
        print()
        print('\a')     # ring bell
        sys.exit()

    if ezColAntBtwnRef < 1:
        print()
        print()
        print()
        print()
        print()
        print(' ========== FATAL ERROR: ', ezColAntBtwnRef, 'is an invalid value for ezColAntBtwnRef')
        print()
        print()
        print()
        print()
        print('\a')     # ring bell
        sys.exit()

    if ezColSecMax < 0.:
        print()
        print()
        print()
        print()
        print()
        print(' ========== FATAL ERROR: ', ezColSecMax, 'is an invalid value for ezColSecMax')
        print()
        print()
        print()
        print()
        print('\a')     # ring bell
        sys.exit()

    if ezColRefAction not in [0, 1, 2]:
        print()
        print()
        print()
        print()
        print()
        print(' ========== FATAL ERROR: ', ezColRefAction, 'is an unrecognized value for ezColRefAction')
        print()
        print()
        print()
        print()
        print('\a')     # ring bell
        sys.exit()




    # fix silly arguments
    if not ezColFileNamePrefix:
        ezColFileNamePrefix = ezRAObsName.split()[0]    # first word of ezRAObsName
    if not ezColCenterFreqRef and ezColUsbRelay:
        ezColCenterFreqRef = ezColCenterFreqAnt

    # print status
    print()
    print('   ezRAObsName =', ezRAObsName)
    print('   ezRAObsLat  =', ezRAObsLat)
    print('   ezRAObsLon  =', ezRAObsLon)
    print('   ezRAObsAmsl =', ezRAObsAmsl)
    print('   ezColFileNamePrefix =', ezColFileNamePrefix)
    print()
    print('   ezColCenterFreqRef  =', ezColCenterFreqRef)
    print('   ezColCenterFreqAnt  =', ezColCenterFreqAnt)
    print('   ezColBandWidth      =', ezColBandWidth)
    print('   ezColSampleRate     =', ezColSampleRate)
    print()
    print('   ezColReadBinQtyBits =', ezColReadBinQtyBits)
    print('   ezColReadBinMin     =', ezColReadBinMin)
    print('   ezColFreqBinQtyBits =', ezColFreqBinQtyBits)
    print()
    print('   ezColGain           =', ezColGain)
    print('   ezColBiasTeeOn      =', ezColBiasTeeOn)
    print('   ezColAntBtwnRef     =', ezColAntBtwnRef)
    print()
    print('   ezColAzDeg =', ezColAzDeg)
    print('   ezColElDeg =', ezColElDeg)
    print('   ezColRaH    =', ezColRaH)
    print('   ezColDecDeg =', ezColDecDeg)
    print('   ezColGLatDeg =', ezColGLatDeg)
    print('   ezColGLonDeg =', ezColGLonDeg)
    print('   coordType =', coordType)
    print()
    print('   ezColVerbose   =', ezColVerbose)
    print('   ezColDashboard =', ezColDashboard)
    print('   ezColDispGrid  =', ezColDispGrid)
    print()
    print('   ezColUsbRelay =', ezColUsbRelay)
    print('   ezColIntegQty =', ezColIntegQty)
    print('   ezColTextFontSize =', ezColTextFontSize)
    print('   ezColSampleMax =', ezColSampleMax)
    print('   ezColSecMax    =', ezColSecMax)
    print('   ezColRefAction =', ezColRefAction)
    print('   ezColYLimL     = [', ezColYLim0, ',', ezColYLim1, ']')



#########################################################################################################
def main():

    global ezRAObsLat                       # float
    global ezRAObsLon                       # float
    global ezRAObsAmsl                      # float
    global ezRAObsName                      # string
    global ezColFileNamePrefix              # string

    global ezColCenterFreqAnt               # float
    global ezColCenterFreqRef               # float
    global ezColBandWidth                   # float
    global ezColSampleRate                  # float

    global ezColReadBinQtyBits              # integer
    global ezColReadBinMin                  # integer
    global ezColFreqBinQtyBits              # integer
    global ezColGain                        # float
    global ezColBiasTeeOn                   # integer
    global ezColAntBtwnRef                  # integer

    global ezColAzDeg                       # float
    global ezColElDeg                       # float
    global ezColRaH                         # float
    global ezColDecDeg                      # float
    global ezColGLatDeg                     # float
    global ezColGLonDeg                     # float
    global coordType                        # integer
    global coord0                           # float
    global coord1                           # float
    global coordMayBeNew                    # integer

    global ezColVerbose                     # integer
    global ezColDashboard                   # integer
    global ezColDispGrid                    # integer

    global ezColUsbRelay                    # integer
    global commandString                    # string
    global dateDayLastS                     # string
    global rmsAvgHistory                    # float array
    global rmsAvgHistoryLenRecent24Hour     # integer
    global rmsAvgHistoryLenRecent1Hour      # integer
    global programState                     # integer
    global refAction                        # integer
    global ezColIntegQty                    # integer
    global ezColRefAction                   # integer
    global ezColYLim0                       # float
    global ezColYLim1                       # float
    global ezColTextFontSize                # integer
    global ezColSampleMax                   # float
    global ezColSecMax                      # float

    printHello()

    ezColArguments()


    # delayed these imports until after possible help screen
    from datetime import date, datetime, timezone
    #import numpy as np
    import operator
    from multiprocessing import Process, Queue
    import matplotlib
    #matplotlib.use('Agg')
    # graphics window was always annoyingly grabbing focus
    #   https://stackoverflow.com/questions/44278369/how-to-keep-matplotlib-python-window-in-background
    matplotlib.use('TkAgg')
    print(' matplotlib.__version__ =', matplotlib.__version__)

    import matplotlib.pyplot as plt
    # ModuleNotFoundError: No module name 'tkinter'
    #   sudo apt-get install python3-tk
    #   sudo apt-get install python3-pil python3-pil.imagetk 
    from matplotlib.widgets import Button, RadioButtons, TextBox

    readBinQty = int(2 ** ezColReadBinQtyBits)
    freqBinQty = int(2 ** ezColFreqBinQtyBits)  # subset of readBinQty

    coordName = [[['azDeg', 'elDeg'], ['raH', 'decDeg'], ['gLatDeg', 'gLonDeg']],
                 [['AzDeg', 'ElDeg'], ['RaH', 'DecDeg'], ['GLatDeg', 'GLonDeg']]]
    # print('coordName[capital][coordType][coord])

    print()
    # https://docs.python.org/3/library/datetime.html
    # ... the recommended way to create an object representing the current time in UTC is
    #   by calling datetime.now(timezone.utc)
    timeStampUtcZero = datetime.now(timezone.utc)           # relative time zero
    print(' timeStampUtcZero =', timeStampUtcZero)
    print()
    timeStampUtcZeroHours = timeStampUtcZero.hour + timeStampUtcZero.minute / 60. \
        + timeStampUtcZero.second / 3600.

    # http://www.astro.sunysb.edu/metchev/AST443/times.html
    # Julian date on January 1.5, 2000 was 2,451,545.0. This is defined as J2000.0
    # MJD = JD - 2,400,000.5
    # JD = MJD + 2,400,000.5
    # https://en.wikipedia.org/wiki/Julian_day
    # MJD has a starting point of midnight on November 17, 1858
    timeStampUtcZeroMjdTimedelta = timeStampUtcZero - datetime(1858, 11, 17, tzinfo=timezone.utc)
    timeStampUtcZeroMjd = timeStampUtcZeroMjdTimedelta.total_seconds() / 60. / 60. / 24.

    # prepare to calculate Local Mean Sidereal Time (LMST) Hours
    # http://www.setileague.org/askdr/lmst.htm
    # Local Mean Sidereal Time (LMST) Hours = south's Right Ascension
    # Local Mean Sidereal Time = LMST = gmstHour + timeStampUtcZeroHours + geodeticLongitude
    # TU = (JD                - 2451545.0) / 36525 is the number of Julian Centuries since J2000.0
    # TU = (MJD + 2,400,000.5 - 2451545.0) / 36525 is the number of Julian Centuries since J2000.0
    # calculated later with each file opening:
    ### tU = (timeStampUtcZeroMjd + (timeStampUtcHourRelThis / 24.) + 2400000.5 - 2451545.0) / 36525.

    # gmstSec (Greenwich Mean Sidereal Time) at 0h UT
    #    = 24110.54841 + 8640184.812866 TU + 0.093104 TU ^ 2 - 6.2x10-6 TU ^ 3
    # calculated later with each file opening:
    ###gmst0UTCSec = 24110.54841 + (8640184.812866 * tU) + (0.093104 * tU * tU) - (6.2e-6 * tU * tU * tU)
    ###gmst0UTCHour = gmst0UTCSec / 60 / 60

    geodeticLongitude = ezRAObsLon / 15.

    # calculated later with each file opening:
    ###lmstZero = gmst0UTCHour + timeStampUtcZeroHours + geodeticLongitude
    lmstZero = -1.      # flag lmstZero as not yet calculated

    # relative history of timeStampUtc in hours
    # timeStampUtcHourRelHistory[0] is most recent, on the right edge of stripcharts
    timeStampUtcHourRelHistory = np.zeros(0)
    rmsAvgHistory = np.full(0, np.nan)
    rmsAvgHistoryLenRecent24Hour = 0
    rmsAvgHistoryLenRecent1Hour  = 0
    rmsAvgHistoryLenRecentMost   = 510

    #freqMinAnt = (ezColCenterFreqAnt - (ezColBandWidth / 2.))   # in MHz
    #freqMaxAnt = freqMinAnt + ezColBandWidth                    # in MHz
    readFreqMinAnt = (ezColCenterFreqAnt - (ezColBandWidth / 2.))   # in MHz
    freqMinAnt = readFreqMinAnt + ezColBandWidth * ezColReadBinMin / readBinQty
    freqMaxAnt = readFreqMinAnt + ezColBandWidth * (ezColReadBinMin + freqBinQty) / readBinQty

    centerFreqAntHz = int(ezColCenterFreqAnt * 1e6)             # in integer Hz
    centerFreqRefHz = int(ezColCenterFreqRef * 1e6)         	# in integer Hz
    bandWidthHz = ezColBandWidth * 1e6                          # in float Hz

    programState = 0                # 0: "To File", 1: Idle, 2: Exit, in case no ezColDashboard
    
    if ezColUsbRelay:
        ezColUsbRelayS = ' Relay'
    else:
        ezColUsbRelayS = ''

    # initialize dashboard
    if ezColDashboard:
        fig = plt.figure(figsize=(20, 12))
        fig.suptitle('ezRA - Easy Radio Astronomy Data Collector - ' + programName, fontsize = 22, y = 0.99)
        grid = fig.add_gridspec(4, 2, hspace=1.0)
        details_ax    = fig.add_subplot(grid[0  , 1])
        powerTime_ax1 = fig.add_subplot(grid[1  , 1])     # Recent  n number of samples
        powerTime_ax2 = fig.add_subplot(grid[2  , 1])     # Recent  1 hour   of samples
        powerTime_ax3 = fig.add_subplot(grid[3  , :])     # Recent 24 hours  of samples
        spectrum_ax   = fig.add_subplot(grid[0:3, 0])
        plt.subplots_adjust(left = 0.07, bottom = 0.08, right = 0.97, top = 0.90, wspace = 0.18, hspace = 0.36)

        powerTime_ax2XB = powerTime_ax2.twiny()  # instantiate a second axes that shares the same y-axis
        powerTime_ax3XB = powerTime_ax3.twiny()  # instantiate a second axes that shares the same y-axis
        spectrum_axXB = spectrum_ax.twiny()  # instantiate a second axes that shares the same y-axis
        spectrum_axXB.set_xlabel('Doppler (MHz)')
        spectrum_axYB = spectrum_ax.twinx()  # instantiate a second axes that shares the same x-axis
        spectrum_axYB.set_ylabel('Fraction of Y Auto Scale')
        spectrum_ax.autoscale(enable = True, axis = 'y')

        bandWidthD2 = ezColBandWidth / 2.                       # ezColBandWidth Divided by 2, in MHz

        #freqMinRef = (ezColCenterFreqRef - bandWidthD2)         # in MHz
        #freqMaxRef = freqMinRef + ezColBandWidth                # in MHz
        readFreqMinRef = (ezColCenterFreqRef - (ezColBandWidth / 2.))   # in MHz
        freqMinRef = readFreqMinRef + ezColBandWidth * ezColReadBinMin / readBinQty
        freqMaxRef = readFreqMinRef + ezColBandWidth * (ezColReadBinMin + freqBinQty) / readBinQty

        freqsAnt = np.linspace(start=freqMinAnt, stop=freqMaxAnt, num=freqBinQty)
        freqsRef = np.linspace(start=freqMinRef, stop=freqMaxRef, num=freqBinQty)

        # dashboard 'NewPlot' button, to clear stripchart plots
        def newPlot(event):
            global rmsAvgHistory
            global rmsAvgHistoryLenRecent24Hour
            global rmsAvgHistoryLenRecent1Hour
            rmsAvgHistory = np.full(0, np.nan)
            rmsAvgHistoryLenRecent24Hour = 0
            rmsAvgHistoryLenRecent1Hour  = 0
        axNewPlot = plt.axes([0.865, 0.95, 0.05, 0.04])
        bNewPlot = Button(axNewPlot, 'New Plot')
        bNewPlot.on_clicked(newPlot)

        # dashboard 'NewFile' button, to start a new data file, and clear stripchart plots
        def newFile(event):
            global dateDayLastS
            dateDayLastS = 'newFileButton'                      # silly value to force new data file
        axNewFile = plt.axes([0.92, 0.95, 0.05, 0.04])
        bNewFile = Button(axNewFile, 'New File')
        bNewFile.on_clicked(newFile)

        # dashboard 'coord0' number entry, change coord0 value live
        def coord0Entry(coord0EntryS):
            global coord0                           # float
            global coordMayBeNew                    # integer
            coord0EntrySplit = coord0EntryS.split()
            if coord0EntrySplit:
                coord0 = float(coord0EntrySplit[0])
                coordMayBeNew = 1
        coord0Entry_ax = fig.add_axes([0.58, 0.81, 0.04, 0.04])
        coord0EntryBox = TextBox(coord0Entry_ax, '')
        coord0EntryBox.on_submit(coord0Entry)
        coord0EntryBox.set_val(str(coord0))         # initialize string of coord0EntryBox

        # dashboard 'coord1' number entry, change coord1 value live
        def coord1Entry(coord1EntryS):
            global coord1                           # float
            global coordMayBeNew                    # integer
            coord1EntrySplit = coord1EntryS.split()
            if coord1EntrySplit:
                coord1 = float(coord1EntrySplit[0])
                coordMayBeNew = 1
        coord1Entry_ax = fig.add_axes([0.58, 0.77, 0.04, 0.04])
        coord1EntryBox = TextBox(coord1Entry_ax, '')
        coord1EntryBox.on_submit(coord1Entry)
        coord1EntryBox.set_val(str(coord1))         # initialize string of coord1EntryBox

        # dashboard 'coordType' radio buttons (no change of coord0 and coord1)
        def coordTypeEntry(label):
            global coordType            # integer
            global coordMayBeNew        # integer
            if label == 'AzEl':
                coordType  = 0
            elif label == 'RaDec':
                coordType = 1
            else:
                # 'GLatGLon'
                coordType = 2
            coordMayBeNew = 1
        radio3_ax = plt.axes([0.625, 0.77, 0.05, 0.08], facecolor='lightgoldenrodyellow')
        radio3 = RadioButtons(radio3_ax, ('AzEl', 'RaDec', 'GLatGLon'))
        radio3.on_clicked(coordTypeEntry)

        # now that radio3 RadioButtons and coordTypeEntry() exist, initialize radio3
        if coordType == 0:              # AzEl
            coordTypeEntry('AzEl')
        elif coordType == 1:            # RaDec
            coordTypeEntry('RaDec')
        else:                           # GLatGLon
            coordTypeEntry('GLatGLon')

        programState = 0                # default "To File"
        programStateQueue = Queue()
        programStateQueue.put(programState)
        # dashboard 'programState' radio buttons, control data collecting priority
        # https://blog.finxter.com/matplotlib-widgets-button/
        def programStateEntry(label):
            global programState                 # integer
            if label == 'To File':
                programState = 0
                programStateQueue.put(programState)
            elif label == 'Idle': 
                programState = 1
                programStateQueue.put(programState)
            else:
                programState = 2
                programStateQueue.put(programState)
                plt.close("all")
                #sdrProcess.join()               # needed ????????????????????
                print('\n\n\n\n\n =============== Try fully killing this program by repeating many Control-C on keyboard ? ....\n\n\n\n\n')
                exit(0)

            print('\n programState =', programState, '    label =', label)
        #radio2_ax = plt.axes([0.865, 0.85, 0.05, 0.09], facecolor='lightgoldenrodyellow')
        radio2_ax = plt.axes([0.92, 0.85, 0.05, 0.09], facecolor='lightgoldenrodyellow')
        radio2 = RadioButtons(radio2_ax, ('To File', 'Idle', 'Exit'))
        radio2.on_clicked(programStateEntry)

        # dashboard 'RefDiv' radio buttons, spectrum divided by (or subtracting) last REF sample
        def refActionFunction(label):
            global refAction
            if label == 'Off':
                refAction = 0
            elif label == 'RefDiv':
                refAction = 1           # plot last Ant spectrum divided by last REF spectrum
            else:
                # 'RefSub'
                refAction = 2           # plot last Ant spectrum after subtracting last REF spectrum
        #radio1_ax = plt.axes([0.92, 0.85, 0.05, 0.09], facecolor='lightgoldenrodyellow')
        radio1_ax = plt.axes([0.865, 0.85, 0.05, 0.09], facecolor='lightgoldenrodyellow')
        #radio1 = RadioButtons(radio1_ax, ('Off', 'RefDiv', 'RefSub'))
        radio1 = RadioButtons(radio1_ax, ('Off', 'RefDiv', 'RefSub'), active=ezColRefAction)
        radio1.on_clicked(refActionFunction)
        #refAction = 0                   # default Off
        refAction = ezColRefAction

        ezColIntegQtyQueue = Queue()
        # dashboard 'ezColIntegQty' number entry, define number of readings per data sample
        def ezColIntegQtyEntry(ezColIntegQtyEntryS):
            global ezColIntegQty                    # integer
            ezColIntegQtyEntrySplit = ezColIntegQtyEntryS.split()
            if ezColIntegQtyEntrySplit:
                ezColIntegQty = int(ezColIntegQtyEntrySplit[0])
                ezColIntegQtyQueue.put(ezColIntegQty)
        ezColIntegQtyEntry_ax = fig.add_axes([0.92, 0.75, 0.05, 0.04])
        ezColIntegQtyEntryBox = TextBox(ezColIntegQtyEntry_ax, 'ezColIntegQty ')
        ezColIntegQtyEntryBox.on_submit(ezColIntegQtyEntry)
        ezColIntegQtyEntryBox.set_val(str(ezColIntegQty))   # initialize string of ezColIntegQtyEntryBox

        # dashboard 'ezColYLimEntry' number pair entry, change displayed spectrum y scale live
        def ezColYLimEntry(ezColYLimEntryS):
            global ezColYLim0                       # float
            global ezColYLim1                       # float
            ezColYLimEntrySplit = ezColYLimEntryS.split()
            if ezColYLimEntrySplit:
                ezColYLim0 = float(ezColYLimEntrySplit[0])
                ezColYLim1 = float(ezColYLimEntrySplit[1])
        ezColYLimEntry_ax = fig.add_axes([0.92, 0.70, 0.05, 0.04])
        ezColYLimEntryBox = TextBox(ezColYLimEntry_ax, 'Fraction of     \nY Auto Scale, \nMin and Max ')
        ezColYLimEntryBox.on_submit(ezColYLimEntry)
        # initialize string of ezColYLimEntryBox
        ezColYLimEntryBox.set_val(str(ezColYLim0) + '   ' + str(ezColYLim1))

        #if os.name == 'nt':     # Windows
        #    # maximize plot window
        #    # https://stackoverflow.com/questions/12439588/how-to-maximize-a-plt-show-window-using-python
        #    mng = plt.get_current_fig_manager()
        #    #mng.window.state('zoomed')

        fig.show()  # show the window (figure will be in foreground, but the user may move it to background)
        #           # (avoid calling plt.show() and plt.pause() to prevent window popping to foreground)

    else:
        # ezColDashboard is 0
        programState = 0                # default "To File"
        programStateQueue = Queue()
        programStateQueue.put(programState)

        ezColIntegQtyQueue = Queue()
        ezColIntegQtyQueue.put(ezColIntegQty)

    commandStringEnd = ' '.join(commandString.split()[1:])

    # if does not exist - create new 'data' directory
    if not os.path.exists('data'):
        os.makedirs('data')
        print(' Created new "data" directory')

    sdrOutQueue = Queue()               #sdr to main communication

    #sdrProcess = Process(target=sdrTask, args=(bandWidthHz, ezColGain, ezColBiasTeeOn, freqBinQty, centerFreqAntHz, centerFreqRefHz, ezColUsbRelay,
    #    ezColVerbose, ezColAntBtwnRef, programStateQueue, ezColIntegQtyQueue, sdrOutQueue))
    sdrProcess = Process(target=sdrTask, args=(bandWidthHz, ezColGain, ezColBiasTeeOn, readBinQty, freqBinQty, ezColReadBinMin,
        centerFreqAntHz, centerFreqRefHz,ezColUsbRelay, ezColVerbose, ezColAntBtwnRef, programStateQueue, ezColIntegQtyQueue, sdrOutQueue))
    # sdrTask is started once, with arguments bandWidthHz, ezColGain, ezColBiasTeeOn, freqBinQty, centerFreqAntHz, centerFreqRefHz, ezColUsbRelay, ezColVerbose, and ezColAntBtwnRef.
    #   It will loop and read the latest inputs from programStateQueue and ezColIntegQtyQueue.
    #   At the end of each loop it will output one tuple through sdrOutQueue.
    #   The tuple includes sdrGain, rmsSpectrum, and dataFlagsS.
    sdrProcess.start()


    dateDayLastS = ''         # silly value to force new data file
    sampleNumber = 0
    fileNameS = ''
    fileSample = 0
    fileNamePostS = 'bcdefghijklmnopqrstuvwxyz'
    timeStampUtcSecRelLast = 0
    rmsSpectrumAntLast = np.ones(freqBinQty)
    rmsSpectrumRefLast = np.ones(freqBinQty)
    lmstLabels1to0to0 = \
        ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10',
        '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '0',
        '1', '2', '3', '4', '5', '6', '7', '8', '9', '10',
        '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '0']
    timeStampUtcSecRelThis = -1.    # force at least one loop below
    firstDraw = 1           # flag to draw the plot the first time without data
    ###mainLoop = 0
    while sampleNumber < ezColSampleMax and timeStampUtcSecRelThis < ezColSecMax:
        ###print(' =========== mainLoop =', mainLoop)

        # exit ezCol ?
        if ezColSampleMax <= sampleNumber:
            programState = 2
            programStateQueue.put(programState)
            plt.close("all")
            #sdrProcess.join()               # needed ????????????????????
            print('\n\n\n\n\n =============== Try fully killing this program by repeating many Control-C on keyboard ? ....\n\n\n\n\n')
            exit(0)

        #get the rmsSpectrum from the queue
        hasNewData = 1
        if firstDraw:
            sdrOut = sdrOutQueue.get()
            firstDraw = 0
        else:
            try:
                sdrOut = sdrOutQueue.get_nowait()
            except:
                hasNewData = 0
                #plt.pause(0.5)       # waiting for sdrOutQueue anyway

        if hasNewData:
            (sdrGain, rmsSpectrum, dataFlagsS) = sdrOut
            feedRef = 'R' in dataFlagsS

            timeStampUtc  = datetime.now(timezone.utc)      # this sample's timestamp
            timeStampUtcS = timeStampUtc.strftime('%Y-%m-%dT%H:%M:%S ')
            # https://stackoverflow.com/questions/44823073/convert-datetime-time-to-seconds
            # datetime.time objects are meant to represent a time of the day.
            # datetime.timedelta objects are meant to represent a duration, and they have a total_seconds() method
            timeStampUtcSecRelThis = (timeStampUtc - timeStampUtcZero).total_seconds()
            timeStampUtcHourRelThis = timeStampUtcSecRelThis / 3600.

            # timeStampUtcS = '2022-12-22T21:19:49 '
            #                  01234567890123456789
            dateDayThisS = timeStampUtcS[8:10]

            # timeStampUtcS = '2022-12-22T21:19:49 '
            #                  01234567890123456789
            timeUtcDateS = timeStampUtcS[:10]
            timeUtcTimeS = timeStampUtcS[11:19]
            # https://stackoverflow.com/questions/4563272/how-to-convert-a-utc-datetime-to-a-local-datetime-using-only-standard-library
            timePcS = timeStampUtc.astimezone(tz=None).strftime('%Y-%m-%dT%H:%M:%S ')
            timePcDateS = timePcS[:10]
            timePcTimeS = timePcS[11:19]

            if lmstZero < 0.0:       # if lmstZero not yet calculated
                # with each new file, update lmstZero (changes about 4 minutes each 24 hours).
                # As described above, calculate Local Mean Sidereal Time (LMST) Hours
                tU = (timeStampUtcZeroMjd + (timeStampUtcHourRelThis / 24.) + 2400000.5 - 2451545.0) \
                    / 36525.
                gmst0UTCSec = 24110.54841 + 8640184.812866 * tU + 0.093104 * tU * tU \
                    - 6.2e-6 * tU * tU * tU
                gmst0UTCHour = gmst0UTCSec / 3600.
                lmstZero = gmst0UTCHour + timeStampUtcZeroHours + geodeticLongitude
                #print('lmstZero % 24 =', lmstZero % 24.)

            # this sample's Local Mean Sidereal Time (LMST) in hours, value at dashboard stripcharts' right edge
            lmstThis = (lmstZero + timeStampUtcHourRelThis) % 24.

            if programState != 1:       # if not "Idle"
                if dateDayLastS != dateDayThisS:
                    # start new data file, because of new UTC day, or newFileButton
                    # if old data file open, close it
                    if len(fileNameS):
                        fileWrite.close()

                    # try to not write over existing data files,
                    # assumes 'data' directory exists
                    # fileNameHourS = 'YYMMDD_HH'
                    #                  0123456789
                    fileNameHourS = timeStampUtcS[2:4] + timeStampUtcS[5:7] + dateDayThisS + '_' \
                        + timeStampUtcS[11:13]
                    fileNameMidS = 'data' + os.path.sep + ezColFileNamePrefix + fileNameHourS
                    # first try fileNameS with no trailing character
                    fileNameS = fileNameMidS + '.txt'
                    if not os.path.exists(fileNameS):       # if fileNameS is available
                        fileNameDashS = ezColFileNamePrefix + fileNameHourS + '.txt'  # for dashboard
                    else:
                        # try fileNameS with one trailing character
                        for i in range(25):
                            fileNameS = fileNameMidS + fileNamePostS[i] + '.txt'
                            if not os.path.exists(fileNameS):       # if fileNameS is available
                                # for dashboard
                                fileNameDashS = ezColFileNamePrefix + fileNameHourS + fileNamePostS[i] + '.txt'
                                break           # get out of FOR loop
                            fileNameS = ''      # to flag no available filenames
                        if not fileNameS:
                            # no available filenames
                            print()
                            print('ERROR: already too many files with same fileName base on this UTC date')
                            print()
                            programState = 2
                            programStateQueue.put(programState)
                            plt.close("all")
                            #sdrProcess.join()               # needed ????????????????????
                            print('\n\n\n\n\n =============== Try fully killing this program by repeating many Control-C on keyboard ? ....\n\n\n\n\n')
                            print('\a')     # ring bell
                            sys.exit()

                    print()
                    print('Starting new output file:', fileNameS, '===============')

                    # with each new file, update lmstZero (changes about 4 minutes each 24 hours).
                    # As described above, calculate Local Mean Sidereal Time (LMST) Hours
                    tU = (timeStampUtcZeroMjd + (timeStampUtcHourRelThis / 24.) + 2400000.5 - 2451545.0) \
                        / 36525.
                    gmst0UTCSec = 24110.54841 + 8640184.812866 * tU + 0.093104 * tU * tU \
                        - 6.2e-6 * tU * tU * tU
                    gmst0UTCHour = gmst0UTCSec / 3600.
                    lmstZero = gmst0UTCHour + timeStampUtcZeroHours + geodeticLongitude
                    #print('lmstZero % 24 =', lmstZero % 24.)

                    # open() with 1 to write to file after every '\n'
                    fileWrite = open(fileNameS, 'w', 1)

                    # write file header
                    fileWrite.write(f'from {programRevision} {commandStringEnd}\n' \
                        + f'lat {ezRAObsLat:g} ' \
                        + f'long {ezRAObsLon:g} ' \
                        + f'amsl {str(ezRAObsAmsl)} ' \
                        + f'name {ezRAObsName}\n' \
                        + f'freqMin {freqMinAnt:g} ' \
                        + f'freqMax {freqMaxAnt:g} ' \
                        + f'freqBinQty {str(freqBinQty)}\n' \
                        + f'{coordName[0][coordType][0]} {coord0:g} ' \
                        + f'{coordName[0][coordType][1]} {coord1:g}\n' \
                        + '# times are in UTC\n'\
                        + f'# gain {str(sdrGain)}\n' \
                        + '# frequency spectrums of RMS power in dB\n')

                    coordMayBeNew = 0

                    dateDayLastS = dateDayThisS
                    fileSample = 0

                # not new file, but if coordType or coord0 or coord1 has changed, write a coord line
                elif coordMayBeNew:
                    fileWrite.write(f'{coordName[0][coordType][0]} {coord0:g} {coordName[0][coordType][1]} {coord1:g}\n')
                    coordMayBeNew = 0

                # now feedRef, timeStampUtcS, fileNameS, and fileSample are updated

                print()
                currentCenterFreq = ezColCenterFreqRef if feedRef else ezColCenterFreqAnt
                print(timeStampUtcS, 'UTC    ', fileNameS, '   ', fileSample, '  ', currentCenterFreq, 'MHz  ', dataFlagsS)
                print('Receiving', ezColIntegQty, 'readings, each with', freqBinQty, 'frequencies ...')

                # write data sample line
                fileWrite.write(timeStampUtcS + ' '.join(f'{i:.9g}' for i in rmsSpectrum) + dataFlagsS + '\n')

                if ezColVerbose:
                    print(ezRAObsName)
                    print(f'  Latitude    {ezRAObsLat:0.1f}')
                    print(f'  Longitude   {ezRAObsLon:0.1f}')
                    print(f'  Amsl        {ezRAObsAmsl:0.0f}')
                    if coordType == 0:              # AzEl
                        print(f'  AzDeg       {coord0:0.1f}')
                        print(f'  ElDeg       {coord1:0.1f}')
                    elif coordType == 1:            # RaDec
                        print(f'  RaH         {coord0:0.1f}')
                        print(f'  DecDeg      {coord1:0.1f}')
                    else:                           # GLatGLon
                        print(f'  GLatDeg     {coord0:0.1f}')
                        print(f'  GLonDeg     {coord1:0.1f}')
                    print(f'FreqBinQty    {freqBinQty:d}')
                    print(f'Gain          {sdrGain:0.1f}')
                    print(f'Integration   {timeStampUtcSecRelThis - timeStampUtcSecRelLast:0.1f}  sec')
                    print( 'ezColIntegQty', ezColIntegQty)
                    print('---')
                    print(f'ezColCenterFreqRef {ezColCenterFreqRef:0.6f}  MHz{ezColUsbRelayS}')
                    print(f'ezColCenterFreqAnt {ezColCenterFreqAnt:0.6f}  MHz')
                    print(f'FreqMin            {freqMinAnt:0.6f}  MHz')
                    print(f'FreqMax            {freqMaxAnt:0.6f}  MHz')
                    print('---')
                    print(fileNameDashS)
                    print('File SampleQty   ', fileSample, dataFlagsS)
                    print('---')
                    print(timeUtcDateS + '   ' + timeUtcTimeS + '  UTC')
                    print(timePcDateS  + '   ' + timePcTimeS  + '  PC')
                    print( \
                        f"approximate Local Mean Sidereal Time (LMST) Hours = meridian's Right Ascension = {lmstThis:0.1f}")
                    #    f"approximate Local Mean Sidereal Time (LMST) Hours = south's Right Ascension = {lmstThis:0.1f}")

            if ezColDashboard:
                # erase dashboard
                details_ax.clear()          # top right text section
                powerTime_ax1.clear()       # top    right "Recent Samples"  stripchart
                powerTime_ax2.clear()       # middle right "Recent One Hour" stripchart
                powerTime_ax3.clear()       # bottom       "Recent 24 Hours" stripchart
                spectrum_ax.clear()         # top left frequency spectrum

                # update top right text section
                details_ax.axis('off')

                # erase previous text
                del fig.texts[2:]

                # write column 1 (left)
                fig.text(0.53, 0.95, \
                    ezRAObsName \
                    + '\n  Latitude\n  Longitude\n  Amsl', \
                    horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)
                fig.text(0.53, 0.76, \
                    '  FreqBinQty\n  Gain\n  Integration', \
                    horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)

                # write column 2
                fig.text(0.61, 0.95, \
                    f'\n{ezRAObsLat:0.1f}\n{ezRAObsLon:0.1f}\n{ezRAObsAmsl:0.0f}\n', \
                    horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)
                fig.text(0.51, 0.83, \
                    f'{coordName[1][coordType][0]}\n\n{coordName[1][coordType][1]}', \
                    horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)
                fig.text(0.55, 0.83, \
                    f'{coord0:0.1f}\n\n{coord1:0.1f}', \
                    horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)
                fig.text(0.61, 0.76, \
                    f'{freqBinQty:d}\n{sdrGain:0.1f}\n{timeStampUtcSecRelThis - timeStampUtcSecRelLast:0.1f}  sec', \
                    horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)

                # write column 3
                if programState == 1:       # if "Idle", print red fileNameDashS inside parentheses
                    fig.text(0.68, 0.95, \
                        'FreqCtrRef\nFreqCtr\nFreqMin\nFreqMax\n\n' \
                        + '\nFile SampleQty\n\n' \
                        + timeUtcDateS + '  ' + timeUtcTimeS + '  UTC\n' \
                        + timePcDateS  + '  ' + timePcTimeS  + '  PC', \
                        horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)
                    fig.text(0.68, 0.95, \
                        '\n\n\n\n\n(' + fileNameDashS + ')', color='red', \
                        horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)
                else:
                    fig.text(0.68, 0.95, \
                        'FreqCtrRef\nFreqCtr\nFreqMin\nFreqMax\n\n' \
                        + fileNameDashS + '\nFile SampleQty\n\n' \
                        + timeUtcDateS + '  ' + timeUtcTimeS + '  UTC\n' \
                        + timePcDateS  + '  ' + timePcTimeS  + '  PC', \
                        horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)

                fig.text(0.68, 0.76, \
                    'ReadBinQty\nezColReadBinMin\nezColBandWidth', \
                    horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)

                # write column 4 (right)
                fig.text(0.77, 0.95, \
                    f'{ezColCenterFreqRef:0.6f}  MHz{ezColUsbRelayS[:2]}\n{ezColCenterFreqAnt:0.6f}  MHz\n' \
                        + f'{freqMinAnt:0.6f}  MHz\n{freqMaxAnt:0.6f}  MHz\n\n\n{fileSample:d}   ' \
                    + dataFlagsS, \
                    horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)

                fig.text(0.77, 0.76, \
                    f'{readBinQty:d}\n{ezColReadBinMin:d}\n{ezColBandWidth:0.3f}  MHz', \
                    horizontalalignment='left', verticalalignment='top', fontsize=ezColTextFontSize)


                # update stripchart data

                # append this sample's average of rmsSpectrum to first position of rmsAvgHistory
                rmsAvgHistory = np.concatenate([ \
                    np.array([sum(rmsSpectrum) / freqBinQty]), rmsAvgHistory ])
                rmsAvgHistoryLenRecent24Hour += 1
                rmsAvgHistoryLenRecent1Hour  += 1

                # append this sample's timeStamp to first position of timeStampUtcHourRelHistory
                timeStampUtcHourRelHistory = np.concatenate([ \
                    np.array([timeStampUtcHourRelThis]), timeStampUtcHourRelHistory ])

                # reduce rmsAvgHistoryLenRecent24Hour index to 24 hours
                timeStampUtcHourRelHistory24Max = timeStampUtcHourRelThis + 24.
                while timeStampUtcHourRelHistory24Max < timeStampUtcHourRelHistory[rmsAvgHistoryLenRecent24Hour - 1]:
                    rmsAvgHistoryLenRecent24Hour -= 1
                # trim span of rmsAvgHistory and timeStampUtcHourRelHistory to 24 hours
                timeStampUtcHourRelHistory = timeStampUtcHourRelHistory[:rmsAvgHistoryLenRecent24Hour]
                rmsAvgHistory              = rmsAvgHistory[             :rmsAvgHistoryLenRecent24Hour]

                # reduce rmsAvgHistoryLenRecent1Hour index to 1 hour
                timeStampUtcHourRelHistory1Max = timeStampUtcHourRelThis + 1.
                while timeStampUtcHourRelHistory1Max < timeStampUtcHourRelHistory[rmsAvgHistoryLenRecent1Hour - 1]:
                    rmsAvgHistoryLenRecent1Hour -= 1

                # reduce rmsAvgHistoryLenRecent to rmsAvgHistoryLenRecentMost
                rmsAvgHistoryLenRecent = min(rmsAvgHistoryLenRecent24Hour, rmsAvgHistoryLenRecentMost)

                # plot using timeStampUtcHourRelHistoryRecent
                timeStampUtcHourRelHistoryRecent = timeStampUtcHourRelThis - timeStampUtcHourRelHistory



                # plot bottom "Recent 24 Hours" stripchart
                powerTime_ax3.plot(timeStampUtcHourRelHistoryRecent, \
                    rmsAvgHistory, \
                    marker = '.', markersize = 2, color = 'r')
                # x-scale increases to the left
                powerTime_ax3.set(xlim = [24., 0.], xticks=range(24, -1, -1), \
                    xlabel = 'Recent 24 Hours', ylabel = 'Relative RMS Power in dB')

                # set top Local Mean Sidereal Time (LMST) x-scale
                lmstThisInt = int(lmstThis)
                powerTime_ax3XB.set(xlim=[lmstThis - 24., lmstThis], \
                    xticks=[lmstThisInt - x for x in [23, 22, 21, 20,
                        19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 
                        9, 8, 7, 6, 5, 4, 3, 2, 1, 0]],
                    xticklabels=lmstLabels1to0to0[lmstThisInt:lmstThisInt + 24], \
                    #xlabel = "Local Mean Sidereal Time (LMST) Hours = south's Right Ascension")
                    xlabel = "Local Mean Sidereal Time (LMST) Hours = meridian's Right Ascension")



                # plot middle right "Recent One Hour" stripchart
                powerTime_ax2.plot(timeStampUtcHourRelHistoryRecent[:rmsAvgHistoryLenRecent1Hour], \
                    rmsAvgHistory[:rmsAvgHistoryLenRecent1Hour], \
                    marker = '.', markersize = 2, color = 'c')
                # x-scale increases to the left
                powerTime_ax2.set(xlim = [1., 0.], xticks=np.linspace(1, 0, num=11, endpoint=True),
                    xlabel = 'Recent One Hour', ylabel = 'Relative RMS Power in dB')

                # set top Local Mean Sidereal Time (LMST) x-scale
                lmstThisInTenths = int(lmstThis * 10.) / 10.        # lmstThis with one decimal digit
                powerTime_ax2XBXticks = \
                    [lmstThisInTenths - x for x in [0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]]
                powerTime_ax2XB.set(xlim=[lmstThis - 1, lmstThis], \
                    xticks=powerTime_ax2XBXticks, \
                    xticklabels=[f'{x:0.1f}' for x in powerTime_ax2XBXticks], \
                    #xlabel = "approximate Local Mean Sidereal Time (LMST) Hours = south's Right Ascension")
                    xlabel = "approximate Local Mean Sidereal Time (LMST) Hours = meridian's Right Ascension")



                # plot top right "Recent Samples" stripchart
                powerTime_ax1.plot(range(rmsAvgHistoryLenRecent), \
                    rmsAvgHistory[:rmsAvgHistoryLenRecent], \
                    marker = '.', markersize = 2, color = 'b')
                # x axis increases to the left
                powerTime_ax1.set(xlim = [rmsAvgHistoryLenRecentMost, 0], \
                    xlabel = 'Recent Samples', ylabel = 'Relative RMS Power in dB')



                # plot top left frequency spectrum
                if feedRef:
                    # update as last REF spectrum
                    rmsSpectrumRefLast = rmsSpectrum
                else:
                    # update as last Ant spectrum
                    rmsSpectrumAntLast = rmsSpectrum

                # refAction from radio1 radio button
                if not refAction:           # if not refDiv nor refSub
                    if feedRef:
                        # plot as Ref spectrum, with REF frequencies
                        spectrum_ax.plot(freqsRef, rmsSpectrum, color = 'r')
                        # plot as Ref spectrum, with REF frequency limit values
                        spectrum_ax.set(xlim = [freqMinRef, freqMaxRef],
                            xlabel = 'Reference Frequency (MHz)', ylabel = 'Relative RMS Power in dB')
                        # plot vertical dashed green line on REF center frequency
                        #spectrum_ax.axvline(ezColCenterFreqRef, color = 'g', linestyle = ':', linewidth = 2)
                        # plot vertical dashed green line on 1420.405 frequency
                        spectrum_ax.axvline(1420.405, color = 'g', linestyle = ':', linewidth = 2)
                    else:
                        # plot as Ant spectrum, with Ant frequencies
                        spectrum_ax.plot(freqsAnt, rmsSpectrum, color = 'r')
                        # plot as Ant spectrum, with Ant frequency limit values
                        spectrum_ax.set(xlim = [freqMinAnt, freqMaxAnt],
                            xlabel = 'Frequency (MHz)', ylabel = 'Relative RMS Power in dB')
                        # plot vertical dashed green line on Ant center frequency
                        #spectrum_ax.axvline(ezColCenterFreqAnt, color = 'g', linestyle = ':', linewidth = 2)
                        # plot vertical dashed green line on 1420.405 frequency
                        spectrum_ax.axvline(1420.405, color = 'g', linestyle = ':', linewidth = 2)

                elif refAction == 1:      # refDiv: plot last Ant spectrum divided by last REF spectrum
                    spectrum_ax.plot(freqsAnt, \
                        tuple(map(operator.truediv, rmsSpectrumAntLast, rmsSpectrumRefLast)), color = 'r')
                    # plot as Ant spectrum, with Ant frequency limit values
                    spectrum_ax.set(xlim = [freqMinAnt, freqMaxAnt],
                        xlabel = 'Frequency (MHz)', ylabel = 'Relative RMS Power in dB')
                    # plot vertical dashed green line on Ant center frequency
                    #spectrum_ax.axvline(ezColCenterFreqAnt, color = 'g', linestyle = ':', linewidth = 2)
                    # plot vertical dashed green line on 1420.405 frequency
                    spectrum_ax.axvline(1420.405, color = 'g', linestyle = ':', linewidth = 2)

                else:
                    # refAction == 2:   # refSub: plot last Ant spectrum after subtracting last REF spectrum
                    spectrum_ax.plot(freqsAnt, \
                        tuple(map(operator.__abs__, \
                        map(operator.sub, rmsSpectrumAntLast, rmsSpectrumRefLast))), color = 'r')
                    # plot as Ant spectrum, with Ant frequency limit values
                    spectrum_ax.set(xlim = [freqMinAnt, freqMaxAnt],
                        xlabel = 'Frequency (MHz)', ylabel = 'Relative RMS Power in dB')
                    # plot vertical dashed green line on Ant center frequency
                    #spectrum_ax.axvline(ezColCenterFreqAnt, color = 'g', linestyle = ':', linewidth = 2)
                    # plot vertical dashed green line on 1420.405 frequency
                    spectrum_ax.axvline(1420.405, color = 'g', linestyle = ':', linewidth = 2)

                # read autoscaled left-side y-scale limit values
                spectrum_axY0, spectrum_axY1 = spectrum_ax.get_ylim()
                spectrum_axY1MY0 = spectrum_axY1 - spectrum_axY0        # full autoscaled ylim range, Y1 Minus Y0
                # use ezColYLim0 and ezColYLim1 to set fraction of autoscaled left-side y-scale limit values
                spectrum_ax.set(ylim = [(ezColYLim0 * spectrum_axY1MY0) + spectrum_axY0,
                    (ezColYLim1 * spectrum_axY1MY0) + spectrum_axY0])

                # set top x-scale limits
                #spectrum_axXB.set_xlim(-bandWidthD2, bandWidthD2)
                #spectrum_axXB.set_xlim(freqMinRef - ezColCenterFreqRef, freqMaxRef - ezColCenterFreqRef)
                spectrum_axXB.set_xlim(freqMinAnt - ezColCenterFreqAnt, freqMaxAnt - ezColCenterFreqAnt)
                # set right-side y-scale limits
                spectrum_axYB.set_ylim(ezColYLim0, ezColYLim1)

                spectrum_ax.grid()      # turn on grid



                plt.draw()

                # graphics window was always grabbing focus
                # https://stackoverflow.com/questions/44278369/how-to-keep-matplotlib-python-window-in-background
                #fig.canvas.draw_idle()
                fig.canvas.flush_events()   # update the plot and take care of window events (like resizing etc.)

            timeStampUtcSecRelLast = timeStampUtcSecRelThis

            # not a "sample" if not written to file
            sampleNumber += 1
            fileSample   += 1
            ###mainLoop = 0

        ###mainLoop += 1

        # go to top of while loop, to start next data sample

    # exit ezCol
    #if not sampleNumber < ezColSampleMax:
    #if not timeStampUtcSecRelThis < ezColSecMax:
    programState = 2
    programStateQueue.put(programState)
    plt.close("all")
    #sdrProcess.join()               # needed ????????????????????
    print('\n\n\n\n\n =============== Try fully killing this program by repeating many Control-C on keyboard ? ....\n\n\n\n\n')
    exit(0)



# https://github.com/byggemandboesen/RadioPy/blob/main/src/core/soapy.py
def readFromStream(self) -> np.ndarray:
    '''
    Read bins into buffer
    '''
    #if self.rxStream == None:
    #    print("Stream has not been started yet. Please run startStream() first!!")
    #    sys.exit()
    
    print('============= above readStream')
    sr = self.sdr.readStream(self.rxStream, [self.buffer], self.bins)
    print('============= sr.ret =', sr.ret)
    if sr.ret < self.bins:
        print(f"Error when reading samples... Received {sr.ret}, but expected {self.bins}")
        sdr.stopStream()
        return

    print('============= above readStream return')
    return self.buffer



# https://github.com/byggemandboesen/RadioPy/blob/main/src/spectral_line.py
#def collectData(sdr, fft_num: int, n_bins: int) -> tuple:
#def collectData(sdr, fft_num: int, readBinQty: int, freqBinQty: int, ezColReadBinMin: int, readBinMax: int) -> tuple:
def collectData(sdr, sdrEzColIntegQty, readBinQty, freqBinQty, ezColReadBinMin, readBinMax):
    # Collects and processes data from a given sdr (instance of SDR)
    #   Returns data    - ndarray with collected data

    # Start a stream from device
    rxStream = sdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CF32)
    sdr.activateStream(rxStream)

    #buffer = np.zeros(n_bins, dtype=np.complex64)
    buffer = np.zeros(readBinQty, dtype=np.complex64)

    #data = np.zeros(n_bins, dtype = np.float16)
    #data = np.zeros(freqBinQty, dtype = np.float16)
    #data = np.zeros(readBinQty, dtype = np.float16)
    data = np.zeros(readBinQty)

    #for i in range(fft_num):   # but got sdr.readStream() returning -4, with (USB buffer??) overruns (stderr 'O')
    # ignore any overruns, just redo sdr.readStream()
    #i = 0
    #while i < fft_num:
    i = sdrEzColIntegQty
    '''
    while i:
        #sr = sdr.readStream(rxStream, [buffer], n_bins)
        sr = sdr.readStream(rxStream, [buffer], readBinQty)
        #if sr.ret == n_bins:
        if sr.ret == readBinQty:
            #data = data + np.fft.fftshift((np.abs(np.fft.fft(buffer)) / n_bins) ** 2)
            #data = data + np.fft.fftshift((np.abs(np.fft.fft(buffer)) / readBinQty) ** 2)
            #data = data + np.fft.fftshift((np.abs(np.fft.fft(buffer))) ** 2)
            #dataThis = np.fft.fftshift(np.abs(np.fft.fft(buffer)))
            #dataThis = np.fft.fftshift(np.abs(np.fft.fft(buffer)[ezColReadBinMin:readBinMax]))
            #dataThis = np.fft.fftshift(np.abs(np.fft.fft(buffer)))
            dataThis = np.abs(np.fft.fft(buffer))
            data += dataThis * dataThis
            #i += 1
            i -= 1
    '''
    while i:
        sr = sdr.readStream(rxStream, [buffer], readBinQty)
        if sr.ret == readBinQty:
            dataThis = np.abs(np.fft.fft(buffer))
            data += dataThis * dataThis
            i -= 1
    #data = 10. * np.log10(data[ezColReadBinMin:readBinMax] / fft_num)
    data = 10. * np.log10(np.sqrt(np.fft.fftshift(data)[ezColReadBinMin:readBinMax] / sdrEzColIntegQty))

    # Stop the stream from device
    sdr.deactivateStream(rxStream)
    sdr.closeStream(rxStream)
    #rxStream = None

    '''
    # Replace bad samples lost from sample drops etc.
    idx = np.where(data == -np.inf)
    #if np.any(idx): ????????
    if np.size(idx) > 0:
        #data[idx] = np.interp(freqs[idx], freqs, data)
        print('Bad samples replaced at:')
        print(idx[0])
    '''

    #print('======================== data.shape =', data.shape)
    #return freqs, data
    return data



#def sdrTask(bandWidthHz, ezColGain, ezColBiasTeeOn, freqBinQty, centerFreqAntHz, centerFreqRefHz,
#        ezColUsbRelay, ezColVerbose, ezColAntBtwnRef, programStateQueue, ezColIntegQtyQueue, sdrOutQueue):
def sdrTask(bandWidthHz, ezColGain, ezColBiasTeeOn, readBinQty, freqBinQty, ezColReadBinMin,
        centerFreqAntHz, centerFreqRefHz, ezColUsbRelay, ezColVerbose, ezColAntBtwnRef, programStateQueue, ezColIntegQtyQueue, sdrOutQueue):
    # sdrTask is started once, with arguments bandWidthHz, ezColGain, ezColBiasTeeOn, freqBinQty,
    #    centerFreqAntHz, centerFreqRefHz, ezColUsbRelay, ezColVerbose, and ezColAntBtwnRef.
    #   It will loop and read the latest inputs from programStateQueue and ezColIntegQtyQueue.
    #   At the end of each loop it will output one tuple through sdrOutQueue.
    #   The tuple includes sdrGain, rmsSpectrum, and dataFlagsS.

    #import numpy as np
    import operator
    from math import sqrt
    from time import sleep
    from sys import exit

    #initialize the SDR

    # https://github.com/byggemandboesen/RadioPy/blob/main/src/core/soapy.py
    import SoapySDR
    from SoapySDR import Device

    print('SoapySDR.Device.enumerate =')
    results = SoapySDR.Device.enumerate()
    for result in results:
        print('    ', result)

    # choose device
    driver = 'rtlsdr'
    driver = 'airspy'
    driver = 'sdrplay'
    sdr = SoapySDR.Device("driver=" + driver)

    # explore sdr device
    rx_chan = 0
    print('sdr.listAntennas =', sdr.listAntennas(SOAPY_SDR_RX, rx_chan))
    # airspy says ('RX',)
    # sdrplay RSP2/RSP2pro says ('Antenna A', 'Antenna B', 'Hi-Z')
    print('sdr.listGains =', sdr.listGains(SOAPY_SDR_RX, rx_chan))
    # airspy says ('LNA', 'MIX', 'VGA')
    # sdrplay RSP2/RSP2pro says ('IFGR', 'RFGR')
    #print('sdr.getFrequencyRange = ', end='')
    print('sdr.getFrequencyRange =')
    sdrFreqs = sdr.getFrequencyRange(SOAPY_SDR_RX, rx_chan)
    for freqRange in sdrFreqs:
        print('    ', freqRange)
    # airspy says 2.4e+07, 1.8e+09
    # sdrplay RSP2/RSP2pro says 1000, 2e+09
    print('sdr.listFrequencies =', list(sdr.listFrequencies(SOAPY_SDR_RX, rx_chan)))
    # airspy says
    # sdrplay RSP2/RSP2pro says ['RF', 'CORR']
    print('sdr.listBandwidths =', list(sdr.listBandwidths(SOAPY_SDR_RX, rx_chan)))
    # airspy says
    # sdrplay RSP2/RSP2pro says [200000.0, 300000.0, 600000.0, 1536000.0, 5000000.0, 6000000.0, 7000000.0, 8000000.0]
    rangeSampleRates = sdr.listSampleRates(SOAPY_SDR_RX, rx_chan)
    print('sdr.listSampleRates =', list(int(sampleRate) for sampleRate in rangeSampleRates))
    # airspy says [10000000, 2500000]
    # sdrplay RSP2/RSP2pro says [62500, 96000, 125000, 192000, 250000, 384000, 500000, 768000, 1000000, 2000000, 2048000, 3000000, 4000000, 5000000, 6000000, 7000000, 8000000, 9000000, 10000000]
    print('sdr.getGain =', sdr.getGain(SOAPY_SDR_RX, rx_chan))
    # airspy says 45.0
    # sdrplay RSP2/RSP2pro says 30.0
    print('sdr.getFrequency =', sdr.getFrequency(SOAPY_SDR_RX, rx_chan))
    # sdrplay RSP2/RSP2pro says 200000000.0

    # initialize device
    sdr.setFrequency(SOAPY_SDR_RX, rx_chan, centerFreqAntHz)      # in integer Hz
    #setPPMOffset(ppm_offset)
    #setPPMOffset(0)

    #self.sdr.setGainMode(SOAPY_SDR_RX, rx_chan, True)     # set automatic gain
    sdr.setGainMode(SOAPY_SDR_RX, rx_chan, False)         # turn off AGC
    sdr.setGain(SOAPY_SDR_RX, rx_chan, ezColGain)         # set gain

    sdrGain = sdr.getGain(SOAPY_SDR_RX, rx_chan)




    '''
    print('===========================   ezColGain           =', ezColGain)
    sdr.setGain(SOAPY_SDR_RX, rx_chan, ezColGain)         # set gain
    #sdr.setGain(SOAPY_SDR_RX, rx_chan, 35.)         # set gain
    #sdr.setGain(SOAPY_SDR_RX, rx_chan, 40.)         # set gain
    #sdr.setGain(SOAPY_SDR_RX, rx_chan, 47.)         # set gain
    #sdr.setGain(SOAPY_SDR_RX, rx_chan, 46.)         # set gain
    #sdr.setGain(SOAPY_SDR_RX, rx_chan, 45.)         # set gain
    #sdr.setGain(SOAPY_SDR_RX, rx_chan, 44.)         # set gain - oscillation ?
    #sdr.setGain(SOAPY_SDR_RX, rx_chan, 15.)         # set gain
    #sdr.setGain(SOAPY_SDR_RX, rx_chan, 5.)         # set gain
    sdrGain = sdr.getGain(SOAPY_SDR_RX, rx_chan)
    print('sdr.getGain =', sdr.getGain(SOAPY_SDR_RX, rx_chan))

    # with no sdr.setGain(),
    # sdr.getGain = 30.0

    # ===========================   ezColGain           = 999.9
    # sdr.getGain = 47.0
    # [ERROR] error in activateStream() - Init() failed: sdrplay_api_Fail

    # with no sdr.setGain(SOAPY_SDR_RX, rx_chan, 35.)
    # sdr.getGain = 35.0

    # with no sdr.setGain(SOAPY_SDR_RX, rx_chan, 40.)
    # sdr.getGain = 40.0

    # with no sdr.setGain(SOAPY_SDR_RX, rx_chan, 47.)
    # sdr.getGain = 47.0
    # [ERROR] error in activateStream() - Init() failed: sdrplay_api_Fail

    # with no sdr.setGain(SOAPY_SDR_RX, rx_chan, 46.)
    # sdr.getGain = 46.0
    # [ERROR] error in activateStream() - Init() failed: sdrplay_api_Fail

    # with no sdr.setGain(SOAPY_SDR_RX, rx_chan, 45.)
    # sdr.getGain = 45.0
    # [ERROR] error in activateStream() - Init() failed: sdrplay_api_Fail

    # with no sdr.setGain(SOAPY_SDR_RX, rx_chan, 44.)
    # sdr.getGain = 44.0

    # with no sdr.setGain(SOAPY_SDR_RX, rx_chan, 15.)
    # sdr.getGain = 15.0

    # with no sdr.setGain(SOAPY_SDR_RX, rx_chan, 5.)
    # sdr.getGain = 5.0
    # and all data values of inf
    '''







    try:
        sdr.setSampleRate(SOAPY_SDR_RX, rx_chan, int(ezColSampleRate * 1e6))

        # Set bandwidth greater than sample rate
        #bws = sdr.getAvailableBandwidths()
        #bws = np.array(sdr.listBandwidths(SOAPY_SDR_RX, rx_chan))
        #print('=============== getAvailableBandwidths =', bws)
        #if bws.size != 0:
        #    bw = bws[bws > int(bandWidthHz)].min()
        #    sdr.setBandwidth(bandwidth = bw)
    except:
        print("Device does not support the selected sample rate!!")
        print('\a')     # ring bell
        sys.exit()

    sdr.setFrequency(SOAPY_SDR_RX, rx_chan, centerFreqAntHz)      # in integer Hz
    #setPPMOffset(ppm_offset)
    #setPPMOffset(0)

    try:
        sdr.setBandwidth(SOAPY_SDR_RX, rx_chan, int(bandWidthHz))
    except:
        print("Not able to set bandwidth")
        print('\a')     # ring bell
        sys.exit()

    # Do hacky integer evaluation
    if np.log2(freqBinQty) % 1 == 0 and freqBinQty > 0:
        sdr.bins = freqBinQty
        sdr.buffer = np.array([0]*freqBinQty, np.complex64)
    else:
        print("Invalid number of freqBinQty. Must be positive and follow exponential with base 2 and an integer power!!")
        print('\a')     # ring bell
        sys.exit()

    #rangeSampleRates = sdr.listSampleRates(SOAPY_SDR_RX, rx_chan)
    #print('sdr.listSampleRates =', list(int(sample_rate) for sample_rate in rangeSampleRates))
    print('sdr.getGain =', sdr.getGain(SOAPY_SDR_RX, rx_chan))
    print('sdr.getFrequency =', sdr.getFrequency(SOAPY_SDR_RX, rx_chan))
    print('sdr.getSampleRate =', sdr.getSampleRate(SOAPY_SDR_RX, rx_chan))
    print('sdr.getBandwidth =', sdr.getBandwidth(SOAPY_SDR_RX, rx_chan))
    print('sdr.getFrequencyCorrection =', sdr.getFrequencyCorrection(SOAPY_SDR_RX, rx_chan))

    # Control SDR BiasTee voltage
    #if 0:                           # tedd
    if 0 <= ezColBiasTeeOn:
        # set/unset SDR BiasTee voltage
        #if sdr.set_bias_tee(ezColBiasTeeOn) < 0:
        # https://github.com/pothosware/SoapyAirspy/issues/10
        if ezColBiasTeeOn == 0:
            # disable bias-tee
            result = sdr.writeSetting("biastee", "false")   # Airspy
        if ezColBiasTeeOn == 1:
            # enable bias-tee
            result = sdr.writeSetting("biastee", "true")    # Airspy
        print('============ sdr.writeSetting("biastee",) =', result)
        '''
            #print('============ Could not set SDR BiasTee voltage')
        else:
            if ezColBiasTeeOn:
                print('SDR BiasTee voltage to ON')
            else:
                print('SDR BiasTee voltage to OFF')
        '''
        sleep(0.2)  # Sleep for x seconds

    # by operating system, initialize (reset) feedRef relay system, if any
    # -ezColUsbRelay    0           No relays driving a feed Dicke reference
    # -ezColUsbRelay   11           1 SPST HID relay,     driving feedRef ON or OFF
    # -ezColUsbRelay   15           1 SPST non-HID relay, driving feedRef ON or OFF
    # -ezColUsbRelay   21           2 SPST HID relays, #1 driving feedRef ON or OFF
    # -ezColUsbRelay   22           2 SPST HID relays, #2 driving feedRef ON or OFF
    # -ezColUsbRelay   29           2 SPST HID relays, driving a latching feedRef relay with pulses
    # https://stackoverflow.com/questions/1854/python-what-os-am-i-running-on
    if ezColUsbRelay:
        if os.name == 'nt':     # Windows
            # hidusb-relay-cmd.exe or serialSend.exe assumed to be in same  directory as ezCol program

            if ezColUsbRelay == 11:
                # ezColUsbRelay = 11: 1 SPST HID relay, driving feedRef ON or OFF
                # for USB Relay that talks HID
                # https://github.com/pavel-a/usb-relay-hid
                # https://github.com/pavel-a/usb-relay-hid/releases/tag/usb-relay-lib_v2.1
                # Assumes hidusb-relay-cmd.exe program is in the same directory as this ezCol program.
                # define relay command strings
                # the command prompt command line
                #      ..\ezRA\hidusb-relay-cmd.exe enum 
                # returned
                #      Board ID=[HW348] State: R1=OFF
                # because of the "R1" on that last line, I use:
                # Use double quotes to allow for spaces in path of Window call of hidusb-relay-cmd.exe
                relayOff0 = '"' + os.path.dirname(__file__) + r'\hidusb-relay-cmd.exe" off 1'
                relayOn0  = '"' + os.path.dirname(__file__) + r'\hidusb-relay-cmd.exe" on 1'
                # initialize relays
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds
            elif ezColUsbRelay == 15:
                # ezColUsbRelay = 15: 1 SPST non-HID relay with serialSend.exe
                # for USB Relay that talks serial
                # define relay command strings
                # https://www.amazon.com/dp/B01CN7E0RQ
                #   and down below in "Customer questions & answers",
                #   see "How to set for 7sec on, 3sec off, 20sec on, 1 sec off. (5 series repeat)"
                #   talks of
                #     c:\disp\serialsend.exe /baudrate 9600 /hex "\xA0\x01\x01\xA2"
                #   and
                #     c:\disp\serialsend.exe /baudrate 9600 /hex "\xA0\x01\x00\xA1"
                # https://batchloaf.wordpress.com/serialsend/
                # https://batchloaf.wordpress.com/2011/12/05/serialsend-a-windows-program-to-send-a-text-word-via-serial-port/
                # Assumes serialSend.exe program is in the same directory as this ezCol program.
                # Use double quotes to allow for spaces in path of Window call of hidusb-relay-cmd.exe
                relayOff0 = '"' + os.path.dirname(__file__) + r'\serialSend.exe" /devnum 11 /noscan /baudrate 9600 /hex "\\xA0\\x01\\x00\\xA1"'
                relayOn0  = '"' + os.path.dirname(__file__) + r'\serialSend.exe" /devnum 11 /noscan /baudrate 9600 /hex "\\xA0\\x01\\x01\\xA2"'
                # initialize relays
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds
            elif ezColUsbRelay == 21:
                # ezColUsbRelay = 21: 2 SPST HID relays, #1 driving feedRef ON or OFF
                # for USB Relay that talks HID
                # https://github.com/pavel-a/usb-relay-hid
                # https://github.com/pavel-a/usb-relay-hid/releases/tag/usb-relay-lib_v2.1
                # Assumes hidusb-relay-cmd.exe program is in the same directory as this ezCol program.
                # define relay command strings
                # the command prompt command line
                #      ..\ezRA\hidusb-relay-cmd.exe enum 
                # returned
                #      Board ID=[BITFT] State: R1=OFF R2=OFF
                # because of the "R1" and "R2" on that last line, I use:
                # Use double quotes to allow for spaces in path of Window call of hidusb-relay-cmd.exe
                relayOff0 = '"' + os.path.dirname(__file__) + r'\hidusb-relay-cmd.exe" off 1'
                relayOn0  = '"' + os.path.dirname(__file__) + r'\hidusb-relay-cmd.exe" on 1'
                # initialize relays
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds
            elif ezColUsbRelay == 22:
                # ezColUsbRelay = 22: 2 SPST HID relays, #2 driving feedRef ON or OFF
                # for USB Relay that talks HID
                # https://github.com/pavel-a/usb-relay-hid
                # https://github.com/pavel-a/usb-relay-hid/releases/tag/usb-relay-lib_v2.1
                # Assumes hidusb-relay-cmd.exe program is in the same directory as this ezCol program.
                # define relay command strings
                # the command prompt command line
                #      ..\ezRA\hidusb-relay-cmd.exe enum 
                # returned
                #      Board ID=[BITFT] State: R1=OFF R2=OFF
                # because of the "R1" and "R2" on that last line, I use:
                # Use double quotes to allow for spaces in path of Window call of hidusb-relay-cmd.exe
                relayOff0 = '"' + os.path.dirname(__file__) + r'\hidusb-relay-cmd.exe" off 2'
                relayOn0  = '"' + os.path.dirname(__file__) + r'\hidusb-relay-cmd.exe" on 2'
                # initialize relays
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds
            elif ezColUsbRelay == 29:
                # ezColUsbRelay = 29: 2 SPST HID relays, driving a latching feedRef relay with pulses
                # for USB Relay that talks HID
                # https://github.com/pavel-a/usb-relay-hid
                # https://github.com/pavel-a/usb-relay-hid/releases/tag/usb-relay-lib_v2.1
                # Assumes hidusb-relay-cmd.exe program is in the same directory as this ezCol program.
                # define relay command strings
                # the command prompt command line
                #      ..\ezRA\hidusb-relay-cmd.exe enum 
                # returned
                #      Board ID=[BITFT] State: R1=OFF R2=OFF
                # because of the "R1" and "R2" on that last line, I use:
                # Use double quotes to allow for spaces in path of Window call of hidusb-relay-cmd.exe
                relayOff0 = '"' + os.path.dirname(__file__) + r'\hidusb-relay-cmd.exe" off 1'
                relayOff1 = '"' + os.path.dirname(__file__) + r'\hidusb-relay-cmd.exe" off 2'
                relayOn0  = '"' + os.path.dirname(__file__) + r'\hidusb-relay-cmd.exe" on 1'
                relayOn1  = '"' + os.path.dirname(__file__) + r'\hidusb-relay-cmd.exe" on 2'
                # initialize relays
                # both relays off
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds
                os.system(relayOff1)
                sleep(0.5) # Sleep for 0.5 seconds
                # pulse 'off relay' 0 once to latch feedRef OFF
                os.system(relayOn0)
                sleep(0.5) # Sleep for 0.5 seconds
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds

        else:                   # (posix) Linux assumed
            # how to define relay command strings
            # https://github.com/darrylb123/usbrelay
            # sudo apt-get update
            # sudo apt-get install usbrelay
            ###os.system('sudo usbrelay BITFT_1=0 BITFT_2=0')
            #####################
            #    Serial: BITFT, Relay: 1 State: ff --- Not Found  <=================== BITFT Not Found !
            #####################
            #    > lsusb -v -d 16c0:05df
            # - output looks just like on https://github.com/darrylb123/usbrelay
            #####################
            #    > sudo usbrelay
            #    Device Found
            #      type: 16c0 05df
            #      path: /dev/hidraw2
            #      serial_number:
            #      Manufacturer: www.dcttech.com
            #      Product:      USBRelay1
            #      Release:      100
            #      Interface:    0
            #      Number of Relays = 1
            #    HW348_1=0  <================================== OK, use HW348_1 not BITFT_1 ==========
            # https://www.npmjs.com/package/node-red-contrib-usb-hid-relay/v/0.2.3
            # says also available are
            #    HW-348
            #    HW-343
            #    HW-341
            #    Models with USB-Relay-1, USB-Relay-2 or USB-Relay-4 printed on the PCB
            #os.system('sudo usbrelay HW348_1=0')        # works !

            # also may be helpful ?:
            #   Human Interface Device (HID)
            #   http://vusb.wikidot.com/project:driver-less-usb-relays-hid-interface
            #       https://github.com/pavel-a/usb-relay-hid
            #       http://vusb.wikidot.com/hosted-projects
            #           http://vusb.wikidot.com/examples
            #               https://www.workinprogress.ca/v-usb-tutorial-software-only-usb-for-mega-tiny/
            #   https://www.giga.co.za/ocart/index.php?route=product/product&product_id=229
            #       - part is out of stock, but has pictures and links to
            #           https://github.com/pavel-a/usb-relay-hid
            #           http://www.giga.co.za/Kit_Drivers/USB_Relay2.zip
            #           https://github.com/darrylb123/usbrelay
            #           and says
            #               Here is an example how to control the relay in command line.
            #               CommandApp_USBRelay.exe [device id] [close / open] [relay nr]
            #                   CommandApp_USBRelay.exe J34EL close 01
            #                   CommandApp_USBRelay.exe J34EL open 01

            # 'sudo ' may want renewal and cause trouble ?
            relaySudoS = ''
            relaySudoS = 'sudo '

            if ezColVerbose:
                relayQuietS = ''
            else:
                # quiet mode
                relayQuietS = ' -q'

            if ezColUsbRelay == 11:
                # ezColUsbRelay = 11: 1 SPST HID relay, driving feedRef ON or OFF
                # for USB Relay that talks HID
                ##os.system('sudo usbrelay BITFT_1=0 BITFT_2=0')
                #os.system('sudo usbrelay HW348_1=0')        # works !
                # define relay command strings
                # the Linux command line
                #      sudo usbrelay
                # returned
                #      Device Found
                #        type: 16c0 05df
                #        path: /dev/hidraw3
                #        serial_number: 
                #        Manufacturer: www.dcttech.com
                #        Product:      USBRelay1
                #        Release:      100
                #        Interface:    0
                #        Number of Relays = 1
                #      HW348_1=0
                # because of that last line, I use:
                relayOff0 = relaySudoS + 'usbrelay' + relayQuietS + ' HW348_1=0'
                relayOn0  = relaySudoS + 'usbrelay' + relayQuietS + ' HW348_1=1'
                # initialize relays
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds
            elif ezColUsbRelay == 15:
                #######################################################
                ################ untested as of 240714 ################
                #######################################################
                # ezColUsbRelay = 15: 1 SPST non-HID relay with serialSend.exe
                # for USB Relay that talks serial
                #relayOff0 = '#'             # Linux: not yet implemented
                #relayOn0  = '#'             # Linux: not yet implemented
                #relayOff0 = 'stty -F /dev/ttyUSB0 9600; sleep 0.1s; echo echo -ne "\\xA0\\x01\\x00\\xA1" > /dev/ttyUSB0' # Working inside batch, Not working inside Python
                #relayOn0  = 'stty -F /dev/ttyUSB0 9600; sleep 0.1s; echo echo -ne "\\xA0\\x01\\x01\\xA2" > /dev/ttyUSB0' # Working inside batch, Not working inside Python
                #relayConf0 = 'sudo chmod 777 /dev/ttyUSB0'     # working
                # Put the user in the dialout group for Serial,
                #   and also in the plugdev group for USB:
                #     sudo usermod -a -G dialout,plugdev <username>
                #   
                # See the group for serial:
                #   ls -l /dev/ttyUSB0
                # gives
                #   crw-rw---- 1 root dialout 188, 0 15 juil. 14:12 /dev/ttyUSB0
                #   
                # See the group for USB:
                #   ls -l /dev/hidraw1
                # gives
                #   crw-rw---- 1 root plugdev 246, 1 15 juil. 14:12 /dev/hidraw1
                relayOff0 = relaySudoS + 'python3 ' + os.path.dirname(__file__) + '/ezSerRelay.py /dev/ttyUSB0 9600 0'
                relayOn0  = relaySudoS + 'python3 ' + os.path.dirname(__file__) + '/ezSerRelay.py /dev/ttyUSB0 9600 1'
                # initialize relays
                os.system(relayOff0)
                sleep(0.5) # Sleep for x seconds
            elif ezColUsbRelay == 21:
                # ezColUsbRelay = 21: 2 SPST HID relays, #1 driving feedRef ON or OFF
                # for USB Relay that talks HID
                relayOff0 = relaySudoS + 'usbrelay' + relayQuietS + ' BITFT_1=0 BITFT_2=0'
                relayOn0  = relaySudoS + 'usbrelay' + relayQuietS + ' BITFT_1=1 BITFT_2=0'
                # initialize relays
                # both relays off
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds
            elif ezColUsbRelay == 22:
                # ezColUsbRelay = 22: 2 SPST HID relays, #2 driving feedRef ON or OFF
                # for USB Relay that talks HID
                relayOff0 = relaySudoS + 'usbrelay' + relayQuietS + ' BITFT_1=0 BITFT_2=0'
                relayOn0  = relaySudoS + 'usbrelay' + relayQuietS + ' BITFT_1=0 BITFT_2=1'
                # initialize relays
                # both relays off
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds
            elif ezColUsbRelay == 29:
                # ezColUsbRelay = 29: 2 SPST HID relays, driving a latching feedRef relay with pulses
                # define relay command strings
                # the Linux command line
                #      sudo usbrelay
                # returned
                #      Device Found
                #        type: 16c0 05df
                #        path: /dev/hidraw3
                #        serial_number: 
                #        Manufacturer: www.dcttech.com
                #        Product:      USBRelay2
                #        Release:      100
                #        Interface:    0
                #        Number of Relays = 2
                #      BITFT_1=0
                #      BITFT_2=0
                # because of those 2 last lines, I use:
                relayOff0 = relaySudoS + 'usbrelay' + relayQuietS + ' BITFT_1=0 BITFT_2=0'
                relayOff1 = relayOff0
                relayOn0  = relaySudoS + 'usbrelay' + relayQuietS + ' BITFT_1=1 BITFT_2=0'
                relayOn1  = relaySudoS + 'usbrelay' + relayQuietS + ' BITFT_1=0 BITFT_2=1'
                # initialize relays
                # both relays off
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds
                #os.system(relayOff1)
                #sleep(0.5) # Sleep for 0.5 seconds
                # pulse 'off relay' 0 once to latch feedRef OFF
                os.system(relayOn0)
                sleep(0.5) # Sleep for 0.5 seconds
                os.system(relayOff0)
                sleep(0.5) # Sleep for 0.5 seconds

    sdrProgramState = 0     # "To File"
    feedRef = 0             # assume last sample was ANT sample
    antB4Ref = 0            # number of ANT samples before next REF sample
    dataFlagsS = ' '
    readBinMax = ezColReadBinMin + freqBinQty
    while 1:
        #print('============= WHILE')
        # update status of 0/1/2 = "To File/Idle/Exit"
        if not programStateQueue.empty():
            sdrProgramState = programStateQueue.get_nowait()

        # maybe exit
        if sdrProgramState == 2: 
            if 0 <= ezColBiasTeeOn:
                # unset SDR BiasTee voltage
                if sdr.set_bias_tee(False) < 0:
                    print('============ Could not set SDR BiasTee voltage')
                else:
                    print('SDR BiasTee voltage OFF')
                sleep(0.2) # Sleep for x seconds

            # shutdown the stream
            '''
            #sdr.deactivateStream(rxStream) #stop streaming
            sdr.closeStream(rxStream)
            '''
            exit(0)

        # take ezColIntegQty readings and create a sample

        # maybe update sdrEzColIntegQty
        if not ezColIntegQtyQueue.empty():
            sdrEzColIntegQty = ezColIntegQtyQueue.get_nowait()
            #fft_num = sdrEzColIntegQty

        #print('============= above')
        if centerFreqRefHz:         # if REF samples requested
            #print('============= ANT')
            if feedRef:
                # last sample had feedRef ON
                if ezColUsbRelay:
                    if ezColUsbRelay == 29:
                        # ezColUsbRelay = 29: 2 SPST HID relays, driving a latching feedRef relay with pulses
                        # pulse 'off relay' 0 once to latch feedRef OFF
                        os.system(relayOn0)
                        sleep(0.5) # Sleep for 0.5 seconds
                        os.system(relayOff0)
                        sleep(0.5) # Sleep for 0.5 seconds
                    else:
                        # ezColUsbRelay = 11: 1 SPST HID relay, driving feedRef ON or OFF
                        # ezColUsbRelay = 15: 1 SPST non-HID relay, driving feedRef ON or OFF
                        # ezColUsbRelay = 21: 2 SPST HID relays, #1 driving feedRef ON or OFF
                        # ezColUsbRelay = 22: 2 SPST HID relays, #2 driving feedRef ON or OFF
                        # set Relay off for feedRef off
                        os.system(relayOff0)
                        sleep(0.5) # Sleep for 0.5 seconds

                antB4Ref = ezColAntBtwnRef - 1  # reset number of Ant samples before next Ref sample
                sdr.setFrequency(SOAPY_SDR_RX, rx_chan, centerFreqAntHz)      # in integer Hz
                feedRef = 0         # this sample will be with feedRef OFF
                dataFlagsS = ' '
            else:
                #print('============= REF')
                #fft_num = sdrEzColIntegQty
                # last sample had feedRef OFF
                # prepare for REF only if necessary
                if 0 < antB4Ref:                # if ANT samples before next REF sample
                    antB4Ref -= 1               #   decrement number of ANT samples before next REF sample
                else:                           # if no ANT samples before next REF sample
                    # Reference sample if enabled
                    if ezColUsbRelay:
                        if ezColUsbRelay == 29:
                            # ezColUsbRelay = 29: 2 SPST HID relays, driving a latching feedRef relay with pulses
                            # pulse 'on relay' 1 once to latch feedRef ON
                            os.system(relayOn1)
                            sleep(0.5) # Sleep for 0.5 seconds
                            os.system(relayOff1)
                            sleep(0.5) # Sleep for 0.5 seconds
                        else:
                            # ezColUsbRelay = 11: 1 SPST HID relay, driving feedRef ON or OFF
                            # ezColUsbRelay = 15: 1 SPST non-HID relay, driving feedRef ON or OFF
                            # ezColUsbRelay = 21: 2 SPST HID relays, #1 driving feedRef ON or OFF
                            # ezColUsbRelay = 22: 2 SPST HID relays, #2 driving feedRef ON or OFF
                            # set Relay on for feedRef on
                            os.system(relayOn0)
                            sleep(0.5) # Sleep for 0.5 seconds

                    sdr.setFrequency(SOAPY_SDR_RX, rx_chan, centerFreqRefHz)      # in integer Hz
                    feedRef = 1         # this sample will be with feedRef ON
                    dataFlagsS = ' R'

        #data = collectData(sdr = sdr, fft_num = sdrEzColIntegQty, n_bins = freqBinQty)
        #data = collectData(sdr = sdr, fft_num = sdrEzColIntegQty, readBinQty, freqBinQty, ezColReadBinMin, readBinMax)
        data = collectData(sdr, sdrEzColIntegQty, readBinQty, freqBinQty, ezColReadBinMin, readBinMax)

        #print('============= data =', data)

        sdrOut = rmsSpectrum = (sdrGain, data, dataFlagsS)

        sdrOutQueue.put(sdrOut)

#run the main process
if __name__ == '__main__':
    main()

# a@u22-221222a:~/ezRABase/ezColS$ python3  ../ezRA/ezColS250521a.py -exzColIntegQty500  -ezColCenterFreqAnt 100 -exzColBandWidth2.5 

# a@u22-221222a:~/ezRABase/ezColS$ python3  ../ezRA/ezColS250521c.py -ezColCenterFreqAnt 100

# from ezColS251107bAeeee.py -ezColIntegQty 13e4 -ezColYLimL 0.75 0.9

# a@LTO15a:~/ezRABase/ezColS$ python3 ../ezRA/ezColS251104aPeeee.py  -ezColReadBinQtyBits 12  -ezColFreqBinQtyBits 10  -ezColReadBinMin 1536  -ezColUsbRelay 21  -ezColGain 10  -ezColIntegQty 31e3

# a@LTO15a:~/ezRABase/ezColS$ python3 ../ezRA/ezColS251107cPeeee.py  -ezColReadBinQtyBits 12  -ezColFreqBinQtyBits 12  -ezColReadBinMin 0  -exzColUsbRelay21  -ezColBiasTeeOn 0  -ezColSampleRate 0  -ezColCenterFreqAnt 1418.405  -ezColGain 10  -ezColYLimL .7 .9  -ezColIntegQty 31e3



